容量计算 在 golang 中,当切片中元素个数大于其容量时,该切片会触发切片扩容.那么扩容后,切片的容量是多少呢?我们应该如何计算呢?
现有如下代码,以 int32
与 int64
为例分别计算切片扩容后的长度和容量.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import ( "flag" "fmt" ) func main () { var arr32 []int32 var arr64 []int64 var num = flag.Int("num" , 0 , "append element num" ) flag.Parse() for i := 0 ; i < *num; i++ { arr32 = append (arr32, int32 (i)) } fmt.Println(len (arr32), cap (arr32)) for i := 0 ; i < *num; i++ { arr64 = append (arr64, int64 (i)) } fmt.Println(len (arr64), cap (arr64)) }
上述代码在 go/1.18.1 版本之前执行结果如下:
1 2 3 4 5 6 7 8 9 513 1024 513 1024 1025 1344 1025 1280
在 go/1.18.1 版本之后执行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 257 512 257 512 513 864 513 848 1024 1344 1024 1280
可以看到切片的扩容后容量大小与 golang 版本及切片中元素类型(主要是元素所占的 bytes 数)有一定的关系.
源码阅读 下面我们通过阅读 golang 切片相关源码来搞清楚产生上述差异的原因
1.18 之前 以 go/1.17.10 为例,我们来尝试阅读切片扩容的逻辑
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 func growslice (et *_type, old slice, cap int ) slice { newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.cap < 1024 { newcap = doublecap } else { for 0 < newcap && newcap < cap { newcap += newcap / 4 } if newcap <= 0 { newcap = cap } } } switch { default : newlenmem = uintptr (cap ) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr (newcap)) capmem = roundupsize(capmem) newcap = int (capmem / et.size) } memmove(p, old.array, lenmem) return slice{p, old.len , newcap} }
因此计算步骤大致可以总结如下:
根据计算公式获取应该扩容的容量.
如果当前切片容量小于 1024,扩容切片容量 newcap = oldcap * 2
如果当前切片容量大于等于 1024,扩容切片容量 newcap = oldcap * 1.25
内存对齐
根据上一步得到的容量及元素计算出所需要的字节数 newcap * element_size
.如 bytes= newcap * 4(int32)
, bytes=newcap * 8(int64)
.
通过 runtime/sizeclasses.go
查表,找到 bytes/obj
列中大于该 bytes 的最小值.
使用该值除以 element_size
即为扩容后的容量大小
1.18 之后 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 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 { for 0 < newcap && newcap < cap { newcap += (newcap + 3 *threshold) / 4 } if newcap <= 0 { newcap = cap } } } switch { default : newlenmem = uintptr (cap ) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr (newcap)) capmem = roundupsize(capmem) newcap = int (capmem / et.size) } memmove(p, old.array, lenmem) return slice{p, old.len , newcap} }
因此计算步骤大致可以总结如下:
根据计算公式获取应该扩容的容量.
如果当前切片容量小于 256 时,扩容切片容量 newcap = oldcap * 2
如果当前切片容量大于等于 1024,扩容切片容量 newcap = oldcap + (oldcap + 256 * 3) /4
内存对齐
根据上一步得到的容量及元素计算出所需要的字节数 newcap * element_size
.如 bytes= newcap * 4(int32)
, bytes=newcap * 8(int64)
.
通过 runtime/sizeclasses.go
查表,找到 bytes/obj
列中大于该 bytes 的最小值.
使用该值除以 element_size
即为扩容后的容量大小
验证 在 1.18.1 版本之前
1 2 3 4 5 6 7 8 9 10 11 12 13 513 1024 513 1024 1025 1344 1025 1280
在 1.18.1 版本之后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 257 512 257 512 513 864 513 848 1024 1344 1024 1280
参考