Go 语言切片是如何扩容的?( 二 )


growslice? 函数定义在 Go 语言的 runtime 包中 , 它的调用是在编译后的代码中实现的 。具体来说 , 当执行 append 操作时 , 编译器会将其转换为类似下面的代码:
 
slice = append(slice, elem) 
在上述代码中 , 如果切片容量不足以容纳新的元素 , 则会调用 growslice? 函数进行扩容 。所以 growslice 函数的调用是由编译器在生成的机器码中实现的 , 而不是在源代码中显式调用的 。
切片扩容策略有两个阶段 , go1.18 之前和之后是不同的 , 这一点在 go1.18 的 release notes 中有说明 。
下面我用 go1.17 和 go1.18 两个版本来分开说明 。先通过一段测试代码 , 直观感受一下两个版本在扩容上的区别 。
package mainimport "fmt"func main() {s := make([]int, 0)oldCap := cap(s)for i := 0; i < 2048; i++ {s = append(s, i)newCap := cap(s)if newCap != oldCap {fmt.Printf("[%d -> %4d] cap = %-4d|after append %-4dcap = %-4dn", 0, i-1, oldCap, i, newCap)oldCap = newCap}}} 
上述代码先创建了一个空的 slice , 然后在一个循环里不断往里面 append 新元素 。
然后记录容量的变化 , 每当容量发生变化的时候 , 记录下老的容量 , 添加的元素 , 以及添加完元素之后的容量 。
这样就可以观察 , 新老 slice 的容量变化情况 , 从而找出规律 。
运行结果(1.17 版本):
 
[0 ->-1] cap = 0|after append 0cap = 1[0 ->0] cap = 1|after append 1cap = 2[0 ->1] cap = 2|after append 2cap = 4[0 ->3] cap = 4|after append 4cap = 8[0 ->7] cap = 8|after append 8cap = 16[0 ->15] cap = 16|after append 16cap = 32[0 ->31] cap = 32|after append 32cap = 64[0 ->63] cap = 64|after append 64cap = 128 [0 ->127] cap = 128|after append 128cap = 256 [0 ->255] cap = 256|after append 256cap = 512 [0 ->511] cap = 512|after append 512cap = 1024[0 -> 1023] cap = 1024|after append 1024cap = 1280[0 -> 1279] cap = 1280|after append 1280cap = 1696[0 -> 1695] cap = 1696|after append 1696cap = 2304 运行结果(1.18 版本):
 
[0 ->-1] cap = 0|after append 0cap = 1[0 ->0] cap = 1|after append 1cap = 2[0 ->1] cap = 2|after append 2cap = 4[0 ->3] cap = 4|after append 4cap = 8[0 ->7] cap = 8|after append 8cap = 16[0 ->15] cap = 16|after append 16cap = 32[0 ->31] cap = 32|after append 32cap = 64[0 ->63] cap = 64|after append 64cap = 128 [0 ->127] cap = 128|after append 128cap = 256 [0 ->255] cap = 256|after append 256cap = 512 [0 ->511] cap = 512|after append 512cap = 848 [0 ->847] cap = 848|after append 848cap = 1280[0 -> 1279] cap = 1280|after append 1280cap = 1792[0 -> 1791] cap = 1792|after append 1792cap = 2560 
根据上面的结果还是能看到区别的 , 具体扩容策略下面边看源码边说明 。
go1.17扩容调用的是 growslice 函数 , 我复制了其中计算新容量部分的代码 。
// src/runtime/slice.gofunc growslice(et *_type, old slice, cap int) slice {// ...newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {if old.cap < 1024 {newcap = doublecap} else {// Check 0 < newcap to detect overflow// and prevent an infinite loop.for 0 < newcap && newcap < cap {newcap += newcap / 4}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {newcap = cap}}}// ...return slice{p, old.len, newcap}} 在分配内存空间之前需要先确定新的切片容量 , 运行时根据切片的当前容量选择不同的策略进行扩容:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量 , 直到新容量大于期望容量;
go1.18// src/runtime/slice.gofunc growslice(et *_type, old slice, cap int) slice {// ...newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {const threshold = 256if old.cap < threshold {newcap = doublecap} else {// Check 0 < newcap to detect overflow// and prevent an infinite loop.for 0 < newcap && newcap < cap {// Transition from growing 2x for small slices// to growing 1.25x for large slices. This formula// gives a smooth-ish transition between the two.newcap += (newcap + 3*threshold) / 4}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {newcap = cap}}}// ...return slice{p, old.len, newcap}}


推荐阅读