slice的扩容策略 以及 append函数

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 当然也不是 16961.25 倍。

1.18版本之后:

在原来的slice 容量oldcap小于256的时候,新 slice 的容量newcap的确是oldcap 的2倍。

但是,当oldcap容量大于等于 256 的时候,情况就有变化了。当向 slice 中添加元素 512 的时候,老 slice 的容量为 512,之后变成了 848,两者并没有符合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 sliceempty slice,然后变成真正的 slice

如果没有为slice分配内存,就去访问的话会报错

go 复制代码
s := []int{5}
s[0] = 1
//如上
相关推荐
哎呦没19 分钟前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch38 分钟前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
杨哥带你写代码2 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
AskHarries3 小时前
读《show your work》的一点感悟
后端
A尘埃3 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23073 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
Marst Code3 小时前
(Django)初步使用
后端·python·django
代码之光_19803 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
编程老船长3 小时前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
IT果果日记3 小时前
DataX+Crontab实现多任务顺序定时同步
后端