这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
引子
今天在学Golang中slice时看到资料上写的slice的扩容机制为:
于是就在自己的电脑上跟着试了一下,发现了一些与资料上不太符合的情况
func main() {
s := make([]int, 10, 10) // 容量为10的切片
fmt.Printf("%d\n", cap(s)) // 10
s = append(s, 1) // 向前一个切片添加一个数据,容量不够了,进行扩容
fmt.Printf("%d\n", cap(s)) // 20(容量为之前容量的两倍)
s1 := make([]int, 257, 257) // 容量为257的切片
fmt.Printf("%d\n", cap(s1)) // 257
s1 = append(s1, 1) // 向前一个切片添加一个数据,容量不够了,进行扩容
fmt.Printf("%d\n", cap(s1)) // 608(并非之前容量的两倍)
s2 := make([]int, 1024, 1024) // 容量为1024的切片
fmt.Printf("%d\n", cap(s2)) // 1024
s2 = append(s2, 1) // 向前一个切片添加一个数据,容量不够了,进行扩容
fmt.Printf("%d\n", cap(s2)) // 1536(并非之前容量的1.25倍)
}
然后就开始漫长的搜索,发现几乎所有的资料描述的都和上面的策略一样,以1024为界限变为原来的2倍和1.25倍 于是就开始看起了源码
源码
源码位置 在源码的growslice然后中,以下是相关策略的部分代码:
func growslice(et *_type, old slice, cap int) slice {
//省略了一些判断
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if 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
}
}
}
}
看了上面源码后发现,在某次go的版本更新后,更新了slice的扩容策略,使得slice在扩容时变得更加平滑。在之前的策略中小于1024是容量直接翻倍,大于1024变为之前的1.25倍,在更新之后slice在容量m小于256时翻倍,在m>=256时 m=m+(m+3*256)/4,随着m数值的增大,m增加的倍数从2趋近到1.25,接下来将这个策略带入到测试代码之后验证是否正确。
测试
func main() {
s := make([]int, 256, 256) // 容量为256的切片
fmt.Printf("%d\n", cap(s)) // 256
s = append(s, 1) // 向前一个切片添加一个数据,容量不够了,进行扩容
fmt.Printf("%d\n", cap(s)) // 512(容量为之前容量的两倍)
s1 := make([]int, 257, 257) // 容量为257的切片
fmt.Printf("%d\n", cap(s1)) //
s1 = append(s1, 1) // 向前一个切片添加一个数据,容量不够了,进行扩容
fmt.Printf("%d\n", cap(s1)) // 608(并非之前容量的两倍,也并非是1.25*m+192)
s2 := make([]int, 1024, 1024) // 容量为1024的切片
fmt.Printf("%d\n", cap(s2)) // 1024
s2 = append(s2, 1) // 向前一个切片添加一个数据,容量不够了,进行扩容
fmt.Printf("%d\n", cap(s2)) // 1536(并非之前容量的1.25倍)
}
根据上述代码发现有些时候cap并不是按照这个策略扩充的,这是怎么回事呢,原来在这个策略后,还会对上述策略生成的新容量进行一次操作,相关源码如下:
var lenmem, newlenmem, capmem uintptr
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == goarch.PtrSize:
lenmem = uintptr(old.len) * goarch.PtrSize
newlenmem = uintptr(cap) * 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 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
得到新容量后,均需要根据slice的类型size,算出新的容量所需的内存情况capmem,然后再进行capmem向上取整,得到新的所需内存,除上类型size,得到真正的最终容量,作为新的slice的容量,这时所得到的才是新的容量。
源码中相关函数(roundupsize)
roundupsize
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize {
if size <= smallSizeMax-8 {
return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
} else {
return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
}
}
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}
var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31}
var size_to_class128 = [(_MaxSmallSize-smallSizeMax)/largeSizeDiv + 1]uint8{31, 32, 33, 34, 35, 36, 36, 37, 37, 38, 38, 39, 39, 39, 40, 40, 40, 41, 42, 42, 43, 43, 43, 43, 43, 44, 44, 44, 44, 44, 44, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 47, 47, 47, 48, 48, 49, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 53, 53, 53, 53, 54, 54, 54, 54, 54, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 61, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66}
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
举个例子
func main() {
s := make([]int, 2, 2)
fmt.Println(cap(s)) //2
s = append(s, 1, 2, 3)
fmt.Println(cap(s)) //6
}
在这里进行扩容大小的计算,传入参数为bit。如果申请空间为5字节40bit,则入参size uintptr值为40; 接着调用了 roundupsize 函数,et.size=8,传入 40,所以divRoundUp返回为= 5;获取 size_to_class8 数组中索引为 5 的元素为 5;获取 class_to_size 中索引为 5 的元素为 48,roundupsize 返回48,表示比特大小。 最终,新的 slice 的容量为 6
总结
由上面例子可以得知在1.18版本之后,slice在进行扩容时不再是和1024进行比较,而是和256进行比较,更新策略变为:
一、appendslice中比较参数
append 发生扩容时,原有切片长度简述为len1,容量cap;添加切片长度len2;则新申请容量是n = len1 + len2; 当新申请容量n大于原有切片容量cap时,调用growslice函数;
二、调用growslice函数
(一)、计算最终申请容量(newcap)
1.如果当前所需容量(cap)大于原先的容量的两倍(doublecap),则最终申请容量(newcap)为当前所需容量(cap)
2.如果条件一不满足,则进行如下判断:
3.如果原切片长度(old.cap)小于256,则最终申请容量(newcap)等于原容量两倍(doublecap);
4.否则,最终申请容量(newcap,初始值为old.cap),执行(newcap += (newcap + 3*threshold) / 4)操作直到大于所需申请容量(cap),如果newcap溢出,最终申请容量(newcap)等于所需容量(cap)
(二)、对最终申请容量进行内存分配
根据slice的类型size,算出最终申请容量(newcap)所需的内存情况capmem,然后再进行capmem向上取整,得到新的所需内存,除上类型size,得到真正的最终容量,作为新的slice的容量