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}}
在分配内存空间之前需要先确定新的切片容量 , 运行时根据切片的当前容量选择不同的策略进行扩容:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量 , 直到新容量大于期望容量;
// 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}}
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- HTTPS是如何保证密文不能被篡改的?
- 让很多人直呼“要失业”,火爆网络的AIGC,到底是什么东西?
- “流动型程序员”指的是什么类型的程序员?
- 张继科|张继科欠不欠巨额赌债我不知道但张继科非常缺钱我倒是早就看出来
- 景甜|乱套了!那些买卖景甜视频的人,你们是疯了吗?
- |万能钓饵的正确用法,渔获差异巨大,原因是少了几个步骤
- 辅警|大量政府员工给辞退!主要是哪些岗位人员?
- 80年代|一盘棋总是有胜负的
- 薏仁红豆芡实水
- 清热消暑糖水