也许你在各种地方了解过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倍这个条件的时候,计算出来的新容量要小于之前计算出的容量,我绘制了一张图表,你可以感受一下:
更加平滑的扩容算法
从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[2]
结论
八股文背会了之后并不就是高枕无忧了,过去的知识随着时间的推移可能会逐渐失效,持续关注社区的各种动态才能与时俱进。
至于新的八股文,我在这里就不总结啦,就留给你作为课下作业吧!
参考资料
关于旧扩容机制的讨论: 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