Slice 扩容机制解惑

大家都知道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 最小求的容量。

  1. s 原来只有 2 个元素,len 和 cap 都为 2,append 了三个元素后,长度变为 5,容量最小要变成 5,即调用 growslice 函数时,传入的第三个参数应该为 5。即 cap=5。而一方面,doublecap 是原 slice容量的 2 倍,等于 4。满足源码中第一个 if 条件,所以 newcap 变成了 5。
  2. 接着调用了 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扩容机制:

  1. 如果:新旧slice的长度和,len(A)+len(B) >原来的容量*2,则容量扩容为新旧长度之和;
A = append(A,B...)
newcap:=roundupsize(len(A)+len(B))

新旧长度之和,即最终slice长度

  1. 如果:最终slice长度<=原来的容量*2,分2种情况,此时和文首扩容结论一致:
    • 如果原slice长度小于1024,则扩容2倍;
    • 如果原slice长度超过1024,则扩容1.25倍
  2. 最后进行内存对齐;

最容易忽视的的细节,再强调一遍:

slice在append时,必须关注最终slice的长度是否超过原容量的2倍

  1. 如果不是,那么就按最开始的结论。
  2. 如果是,则容量就变为新slice的容量。

最后,执行内存对齐操作,这一步是一定不能少的!

如果实在不知道对齐的最终结果,那么你按这个规律来找到最接近的值吧:0、1、2、4、6、8、10、12、14、16、18、20 .... 即2的倍数
就像例二所述,cap=5时,执行内存对齐操作,最终结果是cap=6

最最后,来个小练习:

  1. 最终的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 
}

  1. 扩容为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
}
  1. 扩容为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 ,
}
  1. 新切片长度为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的情况同样适用。

你可能感兴趣的:(Slice 扩容机制解惑)