基于 Go 1.19。
go
的切片我们都知道可以自动地进行扩容,具体来说就是在切片的容量容纳不下新的元素的时候,
底层会帮我们为切片的底层数组分配更大的内存空间,然后把旧的切片的底层数组指针指向新的内存中:
目前网上一些关于扩容倍数的文章都是基于相对旧版本的 Go
的,新版本中,现在切片扩容的时候并不是那种准确的小于多少容量的时候就
2
倍扩容, 大于多少容量的时候就 1.25
倍扩容,其实这个数值多少不是非常关键的,我们只需要知道的是:
在容量较小的时候,扩容的因子更大,容量大的时候,扩容的因子相对来说比较小。
扩容的示例
我们先通过一个简单的示例来感受一下切片扩容是什么时候发生的:
1 2 3 4 5
| var slice = []int{1, 2, 3} fmt.Println(slice, len(slice), cap(slice))
slice = append(slice, 4) fmt.Println(slice, len(slice), cap(slice))
|
在这个例子中,slice
切片初始化的时候,长度和容量都是
3
(容量不指定的时候默认等于长度)。
因此切片已经容纳不下新的元素了,在我们往 slice
中追加一个新的元素的时候, 我们发现,slice
的长度和容量都变了, 长度增加了 1
,而容量变成了原来的
2
倍。
在 1.18 版本以后,旧的切片容量小于 256 的时候,会进行 2 倍扩容。
实际扩容倍数
其实最新的扩容规则在 1.18
版本中就已经发生改变了,具体可以参考一下这个 commit
: runtime:
make slice growth formula a bit smoother。
大概意思是:
在之前的版本中:对于 <1024
个元素,增加
2
倍,对于 >=1024
个元素,则增加
1.25
倍。 而现在,使用更平滑的增长因子公式。 在 256
个元素后开始降低增长因子,但要缓慢。
它还给了个表格,写明了不同容量下的增长因子:
256 |
2.0 |
512 |
1.63 |
1024 |
1.44 |
2048 |
1.35 |
4096 |
1.30 |
从这个表格中,我们可以看到,新版本的切片库容,并不是在容量小于
1024
的时候严格按照 2
倍扩容,大于
1024
的时候也不是严格地按照 1.25
倍来扩容。
growslice 实现
在 go 中,切片扩容的实现是 growslice
函数,位于
runtime/slice.go
中。
growslice
有如下参数:
oldPtr
: 旧的切片的底层数组指针。
newLen
:
新的切片的长度(= oldLen + num
)。
oldCap
: 旧的切片的容量。
num
: 添加的元素数。
et
: 切片的元素类型(也即
element type
)。
返回一个新的切片,这个返回的切片中,底层数组指针指向新分配的内存空间,长度等于
oldLen + num
,容量就是底层数组的大小。
growslice 实现步骤
- 一些特殊情况判断:如
et.size == 0
,切片元素不需要占用空间的情况下,直接返回。
- 根据
newLen
计算新的容量,保证新的底层数组至少可以容纳
newLen
个元素。
- 计算所需要分配的新的容量所需的内存大小。
- 分配新的切片底层数组所需要的内存。
- 将旧切片上的底层数组的数据复制到新的底层数组中。
注意:这个函数只是实现扩容,新增的元素没有在这个函数往切片中追加。
growslice 源码剖析
说明:
- 整数有可能会溢出,所以代码里面会判断
newLen < 0
。
- 如果切片的元素是空结构体或者空数组,那么
et.size == 0
。
- 在计算新切片的容量的时候,会根据切片的元素类型大小来做一些优化。
- 新切片容量所占用的内存大小为
capmem
。
- 新切片所需要的内存分配完成后,会将旧切片的数据复制到新切片中。
- 最后返回指向新的底层数组的切片,其长度为
newLen
,容量为
newcap
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
| func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice { oldLen := newLen - num
if newLen < 0 { panic(errorString("growslice: len out of range")) }
if et.size == 0 { return slice{unsafe.Pointer(&zerobase), newLen, newLen} }
newcap := oldCap doublecap := newcap + newcap if newLen > doublecap { newcap = newLen } else { const threshold = 256 if oldCap < threshold { newcap = doublecap } else { for 0 < newcap && newcap < newLen { newcap += (newcap + 3*threshold) / 4 } if newcap <= 0 { newcap = newLen } } }
var overflow bool var lenmem, newlenmem, capmem uintptr
switch { case et.size == 1: lenmem = uintptr(oldLen) newlenmem = uintptr(newLen) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) case et.size == goarch.PtrSize: lenmem = uintptr(oldLen) * goarch.PtrSize newlenmem = uintptr(newLen) * goarch.PtrSize capmem = roundupsize(uintptr(newcap) * goarch.PtrSize) overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize newcap = int(capmem / goarch.PtrSize) case isPowerOfTwo(et.size): var shift uintptr if goarch.PtrSize == 8 { shift = uintptr(sys.TrailingZeros64(uint64(et.size))) & 63 } else { shift = uintptr(sys.TrailingZeros32(uint32(et.size))) & 31 } lenmem = uintptr(oldLen) << shift newlenmem = uintptr(newLen) << shift capmem = roundupsize(uintptr(newcap) << shift) overflow = uintptr(newcap) > (maxAlloc >> shift) newcap = int(capmem >> shift) capmem = uintptr(newcap) << shift default: lenmem = uintptr(oldLen) * et.size newlenmem = uintptr(newLen) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) capmem = roundupsize(capmem) newcap = int(capmem / et.size) capmem = uintptr(newcap) * et.size }
if overflow || capmem > maxAlloc { panic(errorString("growslice: len out of range")) }
var p unsafe.Pointer if et.ptrdata == 0 { p = mallocgc(capmem, nil, false) memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) } else { p = mallocgc(capmem, et, true) if lenmem > 0 && writeBarrier.enabled { bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.size+et.ptrdata) } } memmove(p, oldPtr, lenmem)
return slice{p, newLen, newcap} }
|
总结
go 的切片在容量较小的情况下,确实会进行 2
倍扩容,但是随着容量的增长,扩容的增长因子会逐渐降低。 新版本的
growslice
实现中,只有容量小于 256
的时候才会进行 2
倍扩容,
然后随着容量的增长,扩容的因子会逐渐降低(但并不是直接降到
1.25
,而是一个相对缓慢的下降)。