前情提要
最近在复习Golang的切片知识,在网上看大佬们的博客讲解,自己在本地测试发现和博客讲的对不上。
自己查阅源码后,发现是Golang版本不同,每个版本对切片扩容机制的实现也不同。
因此保存一篇博客来记录Golang切片的扩容机制,以及如何阅读这部分源码。
尽管之后版本变动,也可以再通过源码的方式来进行查证。
注明:
本博客只是提供一种了解Golang切片扩容机制的方式,仅供参考。
因为不同版本下扩容机制的实现可能不同,小伙伴如果想要了解自己Go版本的切片扩容机制,也可以参考下本博客的方式。
源码分析
Golang源码版本:1.19.5,mac的arm架构。源码下载链接:https://go.dev/dl/
切片本身具有3个基本要素:指向底层数组元素的指针、长度len、容量cap。
切片在append过程中,如果len > cap,就会触发扩容机制。
切片扩容机制相关的源码都位于go/src/runtime包下,切片在触发扩容后会调用runtime.growslice函数。
growslice函数的扩容机制主要代码如下:
func growslice(et *_type, old slice, cap int) slice {
...
newcap := old.cap
doublecap := newcap + newcap
// 如果期望容量 > 当前容量的2倍,扩容到期望容量
if cap > doublecap {
newcap = cap
} else {
// 期望容量 <= 当前容量的2倍
const threshold = 256
// 如果当前容量 < 256,容量翻倍
if old.cap < threshold {
newcap = doublecap
} else {
// 如果当前容量 >= 256
// 循环扩容,初始值为当前容量,每次扩容为上次的25%,外加256*3/4=192,直到新容量 >= 期望容量
for 0 < newcap && newcap < cap {
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
...
}
这里的参数cap看作期望容量,可以理解为append后的元素个数,也就是扩容后的切片长度,期望能够有的容量。
上述代码只会确定切片的大致容量,还需要根据切片中的元素类型大小对齐内存,这部分代码也在growslice函数中。
growslice函数的扩容后内存对齐机制主要代码如下:
func growslice(et *_type, old slice, cap int) slice {
...
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
case et.size == 1:
...
case et.size == goarch.PtrSize: // 元素类型大小为8字节
lenmem = uintptr(old.len) * goarch.PtrSize
newlenmem = uintptr(cap) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize) // 内存对齐,获取对齐后真正要申请的内存大小
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize) // 内存对齐后计算真正的扩容后容量
case isPowerOfTwo(et.size):
...
default:
...
}
...
}
为了方便,这里只考虑切片元素类型为int64的内存对齐机制。
内存对齐时会调用runtime.roundupsize函数对内存进行向上取整,获取对齐后要申请的内存大小,计算出真正扩容后的容量。
roundupsize函数的内存对齐机制代码如下:
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize {
// size < 32768,即扩容后容量 < 4096,需要根据class_to_size数组来对内存向上取整
if size <= smallSizeMax-8 {
// size <= 1024 - 8,即扩容后容量 <= 127
// size_to_class8数组用来获取class_to_size数组的索引
// divRoundUp(size, smallSizeDiv),相当于计算ceil(size / 8)
return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
} else {
// size > 1024 - 8,即扩容后容量 > 127
// size_to_class128数组用来获取class_to_size数组的索引
// divRoundUp(size-smallSizeMax, largeSizeDiv),相当于计算ceil((size - 1024) / 128)
return uintptr(class_to_size[size_to_class128[divRoundUp