Go slice新的扩容机制 - 你背的八股文过时啦

5,090 阅读4分钟

也许你在各种地方了解过slice的扩容机制,甚至是亲自读过源码,但较新版本的Go中,扩容机制已经产生了一些变化。

过去你了解的slice扩容机制可能是这样的:

期望容量大于旧容量的两倍,则直接取期望容量作为slice的新容量,否则判断slice的容量是否小于1024, 如果小于1024则将旧容量x2作为最终容量,如果大于等于1024则每次增长大约1.25倍。以上计算出的最终容量会再进行一次对齐才是真正的切片容量。

但以上情况仅适用于go 1.17及之前的版本中。

旧规则存在的问题

我们知道,slice扩容时会调用runtime.growslice函数(不熟悉slice底层原理的同学可以先看看我之前的这篇《Go语言切片剖析》)。这里我们只关注该函数slice计算容量部分的逻辑,计算方法如下:

// 1.17及以前的版本中
// old指切片的旧容量, cap指期望的新容量
func growslice(old, cap int) int {
    newcap := old
    doublecap := newcap + newcap
    // 如果期望容量大于旧容量的2倍,则直接使用期望容量作为最终容量
    if cap > doublecap {
        newcap = cap
    } else {
        // 如果旧容量小于1024,则直接翻倍
        if old < 1024 {
            newcap = doublecap
        } else {
            // 每次增长大约1.25倍
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    // 这里忽略了对齐操作
    return newcap
}

这个扩容机制令一些人产生了一些困惑,因为它会产生一些“看起来不那么自然的行为”。

比如它计算出来的新容量不是单调递增的,下面的程序会将不同容量slice的扩容结果打印出来:

package main

import (
    "fmt"
)

func main() {
    for i := 0; i < 2000; i += 100 {
        fmt.Println(i, cap(append(make([]bool, i), true)))
    }
}

该程序的输出如下(旧版本的扩容规则):

// 第一列是切片的旧容量
// 第二列是扩容后的容量
0 8
100 208
200 416
300 640
400 896
500 1024
600 1280
700 1408
800 1792
900 2048
1000 2048
1100 1408 <-- 在这个点,扩容后的新容量比上面的容量要小
1200 1536
1300 1792
1400 1792
1500 2048
1600 2048
1700 2304
1800 2304
1900 2688

我们注意到,在slice的容量刚刚触发大于1024增长1.25倍这个条件的时候,计算出来的新容量要小于之前计算出的容量,我绘制了一张图表,你可以感受一下:

更加平滑的扩容算法

go1.18开始,slice容量的计算方法被改为了这样:

// 只关心扩容规则的简化版growslice
func growslice(old, cap int) int {
    newcap := old
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        const threshold = 256 // 不同点1
        if old < threshold {
            newcap = doublecap
        } else {
            for 0 < newcap && newcap < cap {
                newcap += (newcap + 3*threshold) / 4 // 不同点2
            }
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    return newcap
}

新版的扩容算法相较于旧的有两处不同,我在上面的源码中用注释将它们标了出来。

首先是双倍容量扩容的最大阈值从1024降为了256,只要超过了256,就开始进行缓慢的增长。其次是增长比例的调整,之前超过了阈值之后,基本为恒定的1.25倍增长,而现在超过了阈值之后,增长比例是会动态调整的:

初始长度         增长比例
256             2.0
512             1.63
1024            1.44
2048            1.35
4096            1.30

可以看到,随着切片容量的变大,增长比例逐渐向着1.25进行靠拢

这次更改之后,slice扩容整体的增长曲线变得更加平滑:

想要查看关于这次调整的更多信息,可以查看这个commit

结论

八股文背会了之后并不就是高枕无忧了,过去的知识随着时间的推移可能会逐渐失效,持续关注社区的各种动态才能与时俱进。

至于新的八股文,我在这里就不总结啦,就留给你作为课下作业吧!


我是赵不贪,目前专注于Go,欢迎关注我的微信公众号: 比特要塞