func append (s [] T, vs …T) [] T 的增长原理
在学 Go 语言的过程中,了解到切片,同时学到了切片增长函数 append,在使用的过程中发现一些问题,所以研究源码记录一下。
Why
在官方的 A tour of Go 中,有这样一段代码
我发现一个规律:
len=5 cap=6
len=7 cap=8
len=9 cap=10
len=11 cap=12
在 0-5 容量内,len 和 cap 保持一致,在 len>5 对其进行扩容时,Go 似乎总是把 cap+2,然后我写了一段测试代码
发现 512 cap 以内是按照 2 的指数来增长的,和上面代码的区别在于,这里是循环调用 append 增加 1 个元素,所以我非常好奇 append 的底层原理是怎么实现的?
官方文档解释很简洁,当切片 cap 不够时,分配一个新数组存储,然后返回
当我点击进去时只能看到 append 的函数声明
文件位于 go/src/builtin/builtin.go,这个文件只包含声明(declarations),不包含实现(implementations)。它的主要目的是:
- 为IDE提供类型信息
- 为开发者提供文档
- 为编译器提供类型检查信息
寻找源码的过程被迫中断了,去哪找呢?
我尝试问 AI 发现,类似 make
, new
, append
… 这些函数属于 Go 的 builtin
函数,他们不需要导包就能使用,也没有具体的函数体实现,而是由编译器直接处理。
又回到了找源码的问题,编译过程怎么查看?
可以通过指定编译参数来查看编译步骤,比如指定 GOSSAFUNC=main
查看 main 函数。
接下来我对一段简单的 append 代码生成编译阶段:
通过上面步骤会在当前目录生成一个 ssa.html
文件。使用浏览器打开内容如下:
重点看这行代码,append 实际上对应到了 runtime.growslice
。
growslice & nextslicecap
接下来到官方源码目录下找到 runtime.growslice
,位于 src/runtime/slice.go
文件中,函数体如下:
growslice 前面是一堆检查,后面是针对内存的优化处理,暂且略过,主要针对 nextslicecap
的代码进行研究
解释:
- 当 newLen(oldCap+num) > doublecap,直接返回 newLen
- 否则,当 oldCap < 256 时,返回 doublecap,也就是两倍的 oldCap
- 当 oldCap 超过 256 时,增长倍数从 2x -> 1.25x
- 最后是做溢出检查
参考上面的代码,每次扩容的数量应该是:
- 一次性添加多个:1 2 3 4 5 6 7 8 9 10…
- 循环添加一个:1 2 4 8 16 32 64 128 256 512 832…
但是实际上却是:
- 一次性添加多个:1 2 3 4 6 8 10…
- 循环添加一个:1 2 4 8 16 32 64 128 256 512 848…
why?
roundupsize
从 nextslicecap 接着往下走,发现一个函数 roundupsize
,如下所示
growslice 里面通过 roundupsize 重新优化算出 newcap 所占字节数,再重新将字节数转成 newcap 的大小
重点就是下面这串代码,能够把 5->6 832->848,为什么这么做呢?
这样做的原因主要有下面几点:
a) 内存规整:
b) 内存复用:
c) 缓存友好:
小结
本次折腾结束,Go 对内存做了很多处理,能够让我们不手动管理内存也能得到极高的效率,这点确实要比 C 好很多。
疑问:
goarch.PtrSize 代表什么意思?