# 8. slice

# 8.1 底层

  • runtime/slice.go

    type slice struct {
    	array unsafe.Pointer		// 指向底层数组
    	len   int								// 切片元素数量
    	cap   int								// 底层数组容量
    }
    
  • reflect/value.go

    type SliceHeader struct {
    	Data uintptr
    	Len  int
    	Cap  int
    }
    

# 8.2 创建

  • 根据数组创建

    s := arr[0:3]
    
  • 字面量:编译时插入创建数组的代码

    s := []int{1, 2, 3}
    
  • make:运行时创建数组

    s := make([]int, 10)
    

# 8.3 测试

# 8.3.1 字面量创建切片底层

func main() {
	s := []int{1, 2, 3}
	fmt.Println(s)
}

查看 Plan9 汇编代码,运行:

go build -gcflags -S main.go

重点关注 s := []int{1,2,3} 对应的部分:

LEAQ    type.[3]int(SB), AX						#创建一个大小为3,类型为int的数组
PCDATA  $1, $0
NOP
CALL    runtime.newobject(SB)				  #新建一个结构体(slice)的值,并往里面塞3个数组
MOVQ    $1, (AX)
MOVQ    $2, 8(AX)
MOVQ    $3, 16(AX)

# 8.3.2 make 创建切片

func main() {
	s := make([]int, 3)
	fmt.Println(s)
}

查看 Plan9 汇编代码,运行:

go build -gcflags -S main.go

重点关注 s := make([]int, 3) 对应的部分:

LEAQ    type.int(SB), AX
MOVL    $3, BX
MOVQ    BX, CX
PCDATA  $1, $0
CALL    runtime.makeslice(SB) 		#直接调用 makeslice 方法

# 8.4 访问

  • 下标访问
  • range 遍历
  • len 查看切片长度
  • cap 查看数组容量

# 8.5 追加

s := []int{1, 2, 3}
s = append(s, 4, 5)

如果 append 后,len > cap,则需要做扩容。

# 8.6 扩容

底层调用 growslice()方法:

# 8.6.1 Go1.17 及之前的 growslice()

func growslice(et *_type, old slice, cap int) slice {
  // 检查
	...

  // 确定新 cap
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
    // ① 如果新 cap 大于两倍旧 cap,则直接使用新 cap
		newcap = cap
	} else {
    // ② 如果新 cap 小于两倍旧 cap 且旧 cap 小于 1024,则 cap 直接翻倍
		if old.cap < 1024 {
			newcap = doublecap
		} else {
      // ③ 如果新 cap 小于两倍旧 cap 且旧 cap 大于 1024,则每次增长 25%
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
	
  // 新建数组,复制,字节对齐
  ....
}

# 8.6.2 Go1.18 的 growslice()

func growslice(et *_type, old slice, cap int) slice {
  // 检查
	...

  // 确定新 cap
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
    // ① 如果新 cap 大于两倍旧 cap,则直接使用新 cap
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
      // ② 如果新 cap 小于两倍旧 cap 且旧 cap 小于 256,则 cap 直接翻倍
			newcap = doublecap
		} else {
			// ③ 如果新 cap 小于两倍旧 cap 且旧 cap 大于 256,则增加幅度逐渐从 2x 降到 1.25x
      // 原始容量  			扩容系数
      //	 256					2.0
      //	 512          1.63
      //   1024         1.44
      //   2048         1.35
      //   4096         1.30
			for 0 < newcap && newcap < cap {
				newcap += (newcap + 3*threshold) / 4
			}
			...
		}
	}
	
  // 新建数组,复制,字节对齐
  ....
}

总结

  • Go1.17 及以前
    • 如果 newcap 大于 2*oldcap,则直接使用 newcap
    • 否则
      • 如果 oldcap < 1024,则 2*oldcap
      • 如果 oldcap >= 1024,则 1.25*oldcap
  • Go1.18
    • 如果 newcap 大于 2*oldcap,则直接使用 newcap
    • 否则
      • 如果 oldcap < 256,则 2*oldcap
      • 如果 oldcap >= 256,则 oldcap += (oldcap + 3*256) / 4

在 Go1.18 中,优化了切片在容量较大时扩容的策略,让底层数组大小的增长更加平滑:通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从 2 到 1.25 的突变。该 commit 作者给出了几种原始容量下对应的“扩容系数”:

原始容量 扩容系数
256 2.0
512 1.63
1024 1.44
2048 1.35
4096 1.30

PS:slice 在扩容的时候是并发不安全的,在并发访问的时候,需要加锁。

上次更新: 8/13/2022, 11:21:24 AM