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
//如上
相关推荐
paopaokaka_luck1 小时前
基于SpringBoot+Vue的电影售票系统(协同过滤算法)
vue.js·spring boot·后端
IT_10247 小时前
Spring Boot项目开发实战销售管理系统——系统设计!
大数据·spring boot·后端
ai小鬼头8 小时前
AIStarter最新版怎么卸载AI项目?一键删除操作指南(附路径设置技巧)
前端·后端·github
Touper.8 小时前
SpringBoot -- 自动配置原理
java·spring boot·后端
一只叫煤球的猫9 小时前
普通程序员,从开发到管理岗,为什么我越升职越痛苦?
前端·后端·全栈
一只鹿鹿鹿9 小时前
信息化项目验收,软件工程评审和检查表单
大数据·人工智能·后端·智慧城市·软件工程
专注VB编程开发20年9 小时前
开机自动后台运行,在Windows服务中托管ASP.NET Core
windows·后端·asp.net
程序员岳焱9 小时前
Java 与 MySQL 性能优化:MySQL全文检索查询优化实践
后端·mysql·性能优化
一只叫煤球的猫10 小时前
手撕@Transactional!别再问事务为什么失效了!Spring-tx源码全面解析!
后端·spring·面试
旷世奇才李先生10 小时前
Ruby 安装使用教程
开发语言·后端·ruby