Go slice 新的扩容机制

也许你在各种地方了解过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
}

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

比如它计算出来的新容量不是单调递增的,下面的程序会将不同容量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倍这个条件的时候,计算出来的新容量要小于之前计算出的容量,我绘制了一张图表,你可以感受一下:Go slice 新的扩容机制

更加平滑的扩容算法

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扩容整体的增长曲线变得更加平滑:

Go slice 新的扩容机制

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

结论

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

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

参考资料

[1]

关于旧扩容机制的讨论: https://groups.google.com/g/golang-nuts/c/UaVlMQ8Nz3o

[2]

修改扩容机制的commit: https://github.com/golang/go/commit/2dda92ff6f9f07eeb110ecbf0fc2d7a0ddd27f9d


原文始发于微信公众号(梦真日记):Go slice 新的扩容机制

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/167823.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!