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的情况同样适用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 切片的容量 [5]int 是数组,而 []int 是切片。二者看起来相似,实则是根本上不同的数据结构。 切片的数据...
    Kernholz阅读 15,273评论 1 6
  • 前言 今天来说个简单的,也不简单的东西,那就是切片。slice对于golang来说那真的是一个非常常用的东西了,很...
    LinkinStar阅读 267评论 0 1
  • 前言 最近进了煎鱼大佬[https://www.bookstack.cn/read/eddycjy-go/READ...
    Xcxing阅读 486评论 0 1
  • 切片定义 切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持...
    玖零儛阅读 355评论 0 1
  • array 和 slice 看似相似,却有着极大的不同,但他们之间还有着千次万缕的联系 slice 是引用类型、是...
    戚银阅读 943评论 1 4