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
//如上
相关推荐
Gopher29 分钟前
C语言程序设计知识8
后端
m0_748248231 小时前
Spring Framework 中文官方文档
java·后端·spring
m0_748240541 小时前
Springboot项目:使用MockMvc测试get和post接口(含单个和多个请求参数场景)
java·spring boot·后端
LUCIAZZZ1 小时前
SkyWalking快速入门
java·后端·spring·spring cloud·微服务·springboot·skywalking
方圆想当图灵2 小时前
高性能缓存设计:如何解决缓存伪共享问题
后端·代码规范
Long_poem2 小时前
【自学笔记】Spring Boot框架技术基础知识点总览-持续更新
spring boot·笔记·后端
hong_zc3 小时前
SpringBoot 配置文件
java·spring boot·后端
神马都会亿点点的毛毛张3 小时前
【Docker教程】万字长文详解Docker命令
java·运维·后端·docker·容器
朗迹 - 张伟3 小时前
Golang连接使用SqlCipher
开发语言·后端·golang
m0_748257464 小时前
创建一个简单的spring boot+vue前后端分离项目
vue.js·spring boot·后端