slice扩容策略
一般都是在向 slice 追加了元素之后,才会引起扩容,追加元素调用的是 append
函数。
append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 ...
传入 slice,直接追加一个切片。
ini
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
append
函数返回值是一个新的slice,Go编译器不允许调用了 append 函数后不使用返回值,如果不返回值则不能编译通过。
append实际是向底层数组添加元素,但是底层数组的长度是固定的,如果数组已经满了就无法添加。
这个时候再使用append就会返回一个迁移到新内存地址的slice,新数组的长度会增加,又可以放新元素了。
为了应对未来可能的append操作,每次扩容都是右预留量的,否则每使用append就需要扩容成本就太高了。
扩容策略
新 slice 预留的 buffer
大小是有一定规律的。
在golang1.18版本更新之前网上大多数的文章都是这样描述slice的扩容策略的:
当原 slice 容量小于
1024
的时候,新 slice 容量变成原来的2
倍;原 slice 容量超过1024
,新 slice 容量变成原来的1.25
倍。
在1.18版本更新之后,slice的扩容策略变为了:
当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4
bash
if old.cap < threshold {
newcap = doublecap
} else {
//这里的newcap是oldcap , cap是需要的cap
for 0 < newcap && newcap < cap {
newcap += (newcap + 3*threshold) / 4
}
代码实测
在原来的slice容量oldcap
小于1024的时候,新 slice 的容量newcap
的确是oldcap
的2倍。
但是,当oldcap
大于等于 1024
的时候,情况就有变化了。当向 slice 中添加元素 1280
的时候,原来的slice 的容量为 1280
,之后newcap
变成了 1696
,两者并不是 1.25
倍的关系(1696/1280=1.325)。添加完 1696
后,新的容量 2304
当然也不是 1696
的 1.25
倍。
在1.18
版本之后:
在原来的slice 容量oldcap
小于256的时候,新 slice 的容量newcap
的确是oldcap
的2倍。
但是,当oldcap
容量大于等于 256
的时候,情况就有变化了。当向 slice 中添加元素 512
的时候,老 slice 的容量为 512
,之后变成了 8
48,两者并没有符合newcap = oldcap+(oldcap+3*256)/4
的策略(512+(512+3*256)/4)=832。添加完 848
后,新的容量 1280
当然也不是 按照之前策略所计算出的的1252。
如果只看前半部分,现在网上各种文章里说的 newcap
的规律是对的。现实是,后半部分还对 newcap
作了一个内存对齐
,这个和内存分配策略相关。
进行内存对齐之后,新 slice 的容量是要 大于等于
按照前半部分生成的newcap
。(就看了下)
之后,将老 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中。
最后,向 growslice
函数调用者返回一个新的 slice,这个 slice 的长度并没有变化,而容量却增大了。
引申1
go
package main
import "fmt"
func main() {
s := []int{5}
//s 只有一个元素,容量为1,[5]
s = append(s, 7)
//s 扩容,容量变为2,[5, 7]
s = append(s, 9)
//s 扩容,容量变为4,[5, 7, 9]
x := append(s, 11)
//没有扩容,底层数组就变成了 [5, 7, 9, 11]
//注意,此时 s = [5, 7, 9],容量为4;x = [5, 7, 9, 11],容量为4。这里 s 不变
y := append(s, 12)
//这里还是在 s 元素的尾部追加元素,由于 s 的长度为3,容量为4,所以直接在底层数组索引为3的地方填上12。结果:s = [5, 7, 9],y = [5, 7, 9, 12],x = [5, 7, 9, 12],x,y 的长度均为4,容量也均为4
// 此处的append并没有修改s的len,仍然为3,所以s[3]会超出边界
fmt.Println(s, x, y)
}
这里要注意的是,append函数执行完后,返回的是一个全新的 slice,并且对传入的 slice 并不影响 ,并不会改变原slice的len。
len和cap
访问超过len的位置会直接报错,即时len在cap内
go
package main
import "fmt"
func main() {
s := []int{5, 7, 9} //len=3,cap=3
x := append(s, 11) //直接扩容,搬到新的地址,不和s共享底层数组
y := append(s, 12) //直接扩容,搬到新的地址,不和s共享底层数组
s = append(s, 1)
fmt.Println(s, x, y)
// 打印结果
// [5 7 9 1] [5 7 9 11] [5 7 9 12]
z := []int{5, 7}
z = append(z, 9) //len = 3 , cap = 4 ,访问z[3]会超出范围
q := append(z, 11) //z的cap=4,len=3,可以添加,不必扩容,共享底层数组
r := append(z, 12) //z并没有变化,len和cap都没变,11被12覆盖
z = append(z, 1) //12被1覆盖,z的len变为4,z[3]可以检索
fmt.Println(z, q, r)
//打印结果
// [5 7 9 1] [5 7 9 1] [5 7 9 1]
}
引申2
向一个nil的slice添加元素会发生什么
nil slice
或者 empty slice
都是可以通过调用 append 函数来获得底层数组的扩容。
都是调用 mallocgc
来向 Go 的内存管理器申请到一块内存,然后再赋给原来的nil slice
或 empty slice
,然后变成真正的 slice
。
如果没有为slice分配内存,就去访问的话会报错
go
s := []int{5}
s[0] = 1
//如上