大家都知道slice扩容机制,在切片长度小于1024时会扩容为原来的2倍,超过1024扩容为原来的1.25倍
。
其实这仅仅是slice扩容第一步的其中一个条件,还存在第二条件(if cap > doublecap
),并且还有第二步:内存对齐
,看源码你就知道了:
func growslice(et *_type, old slice, cap int) slice {
// ……
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}
// ……
//内存对齐操作
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}
看两个例子吧:
例1:
func main() {
s := []int{1,2}
s = append(s,4)
s = append(s,5)
s = append(s,6)
fmt.Printf("len=%d, cap=%d",len(s),cap(s))
}
若按最开始的结论,你可能会这么想:
原 slice 容量小于 1024,扩容时容量每次增加 1 倍。添加元素 4 的时候,容量变为4;添加元素 5 的时候不变;添加元素 6 的时候容量增加 1 倍,变成 8。所以你的结论是:len=5, cap=8
恭喜你,你的结论是对的!
例2
func main() {
s := []int{1,2}
s = append(s,4,5,6)
fmt.Printf("len=%d, cap=%d",len(s),cap(s))
}
和上面一样,你可能会这么想:
原 slice 容量小于 1024,扩容时容量每次增加 1 倍。添加元素 4 的时候,容量变为4;添加元素 5 的时候不变;添加元素 6 的时候容量增加 1 倍,变成 8。所以你的结论依然是:len=5, cap=8
这是错误的结论!slice扩容其实远没有这么单纯!
growslice
这个函数的参数依次是 元素的类型,老的 slice,新 slice 最小求的容量。
- s 原来只有 2 个元素,len 和 cap 都为 2,append 了三个元素后,长度变为 5,容量最小要变成 5,即调用 growslice 函数时,传入的第三个参数应该为 5。即 cap=5。而一方面,doublecap 是原 slice容量的 2 倍,等于 4。
满足源码中第一个 if 条件
,所以 newcap 变成了 5。 - 接着调用了 roundupsize 函数,传入 40 (5 乘以8byte),然后进行内存对齐操作,这块涉及到golang内存分配机制,在此不细述,最终的结果是
6
(文末会总结计算方式)
因此正确的运行结果是:len=5, cap=6
例3:
func main() {
s := []int{1,2,3,4,5}
s = append(s,6,7)
fmt.Printf("len=%d, cap=%d",len(s),cap(s))
}
按最开始的结论,你可能会这么想:
原 slice 容量小于 1024,扩容时容量每次增加 1 倍,添加元素 6 的时候,容量变为10;添加元素7的时候,容量还是够的,不会扩容,依然为10.所以你的结论是:len=7, cap=10
那么恭喜你这个时候是结论又是对的!
懵逼了没?崩溃了没?
为了避免困惑,我们来总结一下吧:
slice扩容机制:
- 如果:新旧slice的长度和,len(A)+len(B)
>
原来的容量*2,则容量扩容为新旧长度之和;
A = append(A,B...)
newcap:=roundupsize(len(A)+len(B))
新旧长度之和,即最终slice长度
- 如果:最终slice长度
<=
原来的容量*2,分2种情况,此时和文首扩容结论一致:- 如果原slice长度小于1024,则扩容2倍;
- 如果原slice长度超过1024,则扩容1.25倍
- 最后进行内存对齐;
最容易忽视的的细节,再强调一遍:
slice在append时,必须关注最终slice的长度是否超过原容量的2倍
- 如果不是,那么就按最开始的结论。
- 如果是,则容量就变为新slice的容量。
最后,执行内存对齐操作,这一步是一定不能少的!
如果实在不知道对齐的最终结果,那么你按这个规律来找到最接近的值吧:0、1、2、4、6、8、10、12、14、16、18、20 ....
即2的倍数
就像例二所述,cap=5时,执行内存对齐操作,最终结果是cap=6
最最后,来个小练习:
- 最终的slice长度超过了旧的slice的长度的2倍,即使原容量扩容2倍也放不下,咋整?此时容量按总长度计算,至少为
2+9=11
,执行内存对齐后为12
func main() {
s := []int{1,2}
s = append(s,1,2,3,4,5,6,7,8,9)
fmt.Printf("len=%d, cap=%d",len(s),cap(s)) //len=11, cap=12
}
- 扩容为2倍,正好能放下
func main() {
s := []int{1,2}
s = append(s,3,4)
fmt.Printf("len=%d, cap=%d",len(s),cap(s)) //len=4, cap=4
}
- 扩容为2倍,能放下而且还有空余
func main() {
s := []int{1,2,3}
s = append(s,4,5)
fmt.Printf("len=%d, cap=%d",len(s),cap(s)) //len=5, cap=6 ,
}
- 新切片长度为0,容量不为0,不进行扩容
func main() {
s := []int{1,2,3}
s1 := make([]int,0,4)//len=0 cap=4
s = append(s,s1...)
fmt.Printf("len=%d, cap=%d",len(s),cap(s)) // len=3, cap=3,
}
5.新切片长度不为0时
func main() {
s := []int{1,2,3}
s1 := make([]int,4,4)//len=0 cap=4
s = append(s,s1...)
fmt.Printf("len=%d, cap=%d",len(s),cap(s)) //len=7, cap=8
}
示例4、5说明是按被追加切片的长度(而不是容量)来计算扩容的。
以上示例是针对长度小于1024的情况,大于1024的情况同样适用。