# golang数组是怎么扩容的? **Published by:** [hundredwz](https://paragraph.com/@hundredwz/) **Published on:** 2022-01-18 **URL:** https://paragraph.com/@hundredwz/golang-2 ## Content 说到这个问题,大部分人第一反应都是 数组cap小于1024,是double扩容,如果超出1024,就每次增加1/4扩容 但是,真的是这样吗? 试验 从0开始 首先上代码,大家猜一下输出的是什么? package main import ( "fmt" ) func main() { fmt.Println("------------------int test-----------------") intCap() fmt.Println("------------------bool test----------------") boolCap() fmt.Println("------------------struct test--------------") structCap() } func intCap() { arr := make([]int, 0) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, 1) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, 2) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, 3) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, 4) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, 5) fmt.Println("len:", len(arr), ", cap:", cap(arr)) } func boolCap() { arr := make([]bool, 0) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, false) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, false) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, false) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, false) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, false) fmt.Println("len:", len(arr), ", cap:", cap(arr)) } func structCap() { arr := make([]struct{}, 0) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, struct {}{}) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, struct {}{}) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, struct {}{}) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, struct {}{}) fmt.Println("len:", len(arr), ", cap:", cap(arr)) arr = append(arr, struct {}{}) fmt.Println("len:", len(arr), ", cap:", cap(arr)) } 大家猜好后,看看实际的输出,是否是一样呢? ------------------int test----------------- len: 0 , cap: 0 len: 1 , cap: 1 len: 2 , cap: 2 len: 3 , cap: 4 len: 4 , cap: 4 len: 5 , cap: 8 ------------------bool test---------------- len: 0 , cap: 0 len: 1 , cap: 8 len: 2 , cap: 8 len: 3 , cap: 8 len: 4 , cap: 8 len: 5 , cap: 8 ------------------struct test-------------- len: 0 , cap: 0 len: 1 , cap: 1 len: 2 , cap: 2 len: 3 , cap: 3 len: 4 , cap: 4 len: 5 , cap: 5 从1024开始 上下代码,我们再猜测下输出是什么? package main import ( "fmt" ) func main() { fmt.Println(1024*1.25, 1024*1.25*1.25, 1024*1.25*1.25*1.25, 1024*1.25*1.25*1.25*1.25) // 备注:以上结果输出 1280 1600 2000 2500 fmt.Println("------------------int test-----------------") intCap() fmt.Println("------------------bool test----------------") boolCap() fmt.Println("------------------struct test--------------") structCap() } func intCap() { arr := make([]int, 1024, 1024) fmt.Println("len:", len(arr), ", cap:", cap(arr)) newArr := make([]int, 1280-1024, 1280-1024) arr = append(arr, newArr...) fmt.Println("len:", len(arr), ", cap:", cap(arr)) newArr = make([]int, 1600-1280, 1600-1280) arr = append(arr, newArr...) fmt.Println("len:", len(arr), ", cap:", cap(arr)) newArr = make([]int, 2000-1600, 2000-1600) arr = append(arr, newArr...) fmt.Println("len:", len(arr), ", cap:", cap(arr)) } func boolCap() { arr := make([]bool, 1024, 1024) fmt.Println("len:", len(arr), ", cap:", cap(arr)) newArr := make([]bool, 1280-1024, 1280-1024) arr = append(arr, newArr...) fmt.Println("len:", len(arr), ", cap:", cap(arr)) newArr = make([]bool, 1600-1280, 1600-1280) arr = append(arr, newArr...) fmt.Println("len:", len(arr), ", cap:", cap(arr)) newArr = make([]bool, 2000-1600, 2000-1600) arr = append(arr, newArr...) fmt.Println("len:", len(arr), ", cap:", cap(arr)) } func structCap() { arr := make([]struct{}, 1024, 1024) fmt.Println("len:", len(arr), ", cap:", cap(arr)) newArr := make([]struct{}, 1280-1024, 1280-1024) arr = append(arr, newArr...) fmt.Println("len:", len(arr), ", cap:", cap(arr)) newArr = make([]struct{}, 1600-1280, 1600-1280) arr = append(arr, newArr...) fmt.Println("len:", len(arr), ", cap:", cap(arr)) newArr = make([]struct{}, 2000-1600, 2000-1600) arr = append(arr, newArr...) fmt.Println("len:", len(arr), ", cap:", cap(arr)) } 猜好后,我们再看下结果是什么? ------------------int test----------------- len: 1024 , cap: 1024 len: 1280 , cap: 1280 len: 1600 , cap: 1696 len: 2000 , cap: 2304 ------------------bool test---------------- len: 1024 , cap: 1024 len: 1280 , cap: 1280 len: 1600 , cap: 1792 len: 2000 , cap: 2304 ------------------struct test-------------- len: 1024 , cap: 1024 len: 1280 , cap: 1280 len: 1600 , cap: 1600 len: 2000 , cap: 2000 是不是发现,这个输出完全不按照预想的来呀! 接下来,我们就一点一点的探究,golang的slice到底是怎么扩容的 源码分析 首先,我们要知道,golang的append不是一个函数实现,可以简单的理解为一个语法糖(不准确,感受意思就好)。在程序编译期间,会将append进行语法分析,最终会调用到runtime/slice.go/growslice函数。具体编译可参见参考[1]和[2],此处不再赘述。 我们来看下程序的代码实现。这段代码比较长,我们分段来看。 空值扩容 # https://github.com/golang/go/blob/go1.16.10/src/runtime/slice.go#L125 // et是slice的类型,old是原slice,cap是新的slice的总长度 func growslice(et *_type, old slice, cap int) slice { ...... // 如果新容量小于原容量,直接报错 // 可能导致的原因:比如数据长度溢出 // 在amd64机器上,原数组cap是math.MaxInt64,再增加一个数据,会直接变成1<<63,成为负数 if cap < old.cap { panic(errorString("growslice: cap out of range")) } // 注意这一句,如果类型的size为0,直接返回一个slice类型,cap是新的slice的总长度 if et.size == 0 { return slice{unsafe.Pointer(&zerobase), old.len, cap} } 实际上,到这个地方,我们就能理解,为什么我们前文所有关于struct{}(空类型)的测试,长度只会递增,而不会扩容了! 是因为struct{}的大小为0,golang直接就在原数据基础上增加了1个长度,然后就返回了 熟悉的方案 我们继续往后面看代码 newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.cap < 1024 { newcap = doublecap } else { for 0 < newcap && newcap < cap { newcap += newcap / 4 } if newcap <= 0 { newcap = cap } } } 这段代码就是我们比较熟悉的扩容方案了,扩容按照以下方案进行 如果期望cap超出原cap的两倍,扩容大小就是期望cap 否则 原cap不超过1024,扩容大小是原cap的2倍 超过1024,按照那个1.25的倍率逐渐扩容 1.25倍率如果数值溢出,扩容大小就是期望的cap 可以发现,这个方案跟我们熟悉的扩容机制很相似了,但是跟实际情况并不一样呀! 内存对齐 继续往后面读代码,后面还有处理逻辑 var overflow bool var lenmem, newlenmem, capmem uintptr // Specialize for common values of et.size. // For 1 we don't need any division/multiplication. // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant. // For powers of 2, use a variable shift. switch { // slice类型为1 case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) // slice类型大小为指针大小 case et.size == sys.PtrSize: lenmem = uintptr(old.len) * sys.PtrSize newlenmem = uintptr(cap) * sys.PtrSize capmem = roundupsize(uintptr(newcap) * sys.PtrSize) overflow = uintptr(newcap) > maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize) // slice类型大小为2的指数 case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 } lenmem = uintptr(old.len) << shift newlenmem = uintptr(cap) << shift capmem = roundupsize(uintptr(newcap) << shift) overflow = uintptr(newcap) > (maxAlloc >> shift) newcap = int(capmem >> shift) // 其他情况 default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) capmem = roundupsize(capmem) newcap = int(capmem / et.size) } 这段代码有点长,我们一点一点来解读。 slice大小为1 slice已有数据占用的内存为数据长度(因为size为1,所以内存就等于长度) 新长度所占内存为预期容量大小 总分配内存大小是待分配大小的向上取整值 总分配内存大小如果大于可分配内存空间,则数据溢出 最终要分配的cap就是向上取整后的大小 这时出现了一个新的概念向上取整(roundupsize),我们先记住,在这段处理逻辑中,最终分配的cap需要进行向上取整,后面再看具体怎么向上取整的。 slice大小为PtrSize 首先我们知道,PtrSize的定义如下 const PtrSize = 4 << (^uintptr(0) >> 63) 可知,在32位机器上,PtrSize就是4,64位是8。 我们可以发现这块的计算逻辑和slice元素size为1的没有太大区别,就是计算各种容量的时候乘上了PtrSize slice大小为2的n次方 这块的计算比较底层一些,或者说是为了加快计算,而使用了比较底层的计算方式。 ... shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 lenmem = uintptr(old.len) << shift ... 解释完这两句,就都能理解了。 首先Ctz64是计算64位机器上,et.Size(比如为4),二进制序列有几个0(相关解析可参考[3]) 可知后面有两个0,那么与63进行下位与运算 1 1 1 1 1 1 # 63 & 0 0 0 0 1 0 # 4 = 0 0 0 0 1 0 # 2 可知shift值是2 我们又知道,计算机中,一个数m左移n位=m * 2^n 对于slice元素容量为2的n次方的类型,分配的容量大小一定是 len * et.size = len * 2^n = len << n 所以,我们就能理解前面代码段的含义了,实质就是一个乘法计算总大小,但是改成了位移计算。 为啥要改呢? 因为计算机计算位移的效率高于乘法 slice大小不满足以上条件 好了,现在slice的大小不满足以上条件,那就也没什么骚操作了,直接乘吧,计算最终的大小。 内存对齐探究 看完这一段代码,我们大体知道了,slice的扩容容量就是之前理解的[1.5倍增长、2倍扩容]这种值,再进行向上取整,也就是内存对齐的操作。 具体怎么计算的呢? func roundupsize(size uintptr) uintptr { if size < _MaxSmallSize { if size <= smallSizeMax-8 { return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]]) } else { return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]]) } } if size+_PageSize < size { return size } return alignUp(size, _PageSize) } 首先,我们知道一些前置知识,golang内存管理基于tcmalloc,将不同对象按照内存大小分为以下几种(更多可见[4]、[5]、[6]) 微小对象(size< 16byte) 小对象 (16byte <= size <=32k ) 大对象 (size > 32k) 小对象 如果需要分配内存的是小对象, 且size小于1024,会通过size_to_class8选取出最后分配多少 否则会通过size_to_class128选取分配大小 比如,我们需要分配的内存大小为100,可知divRoundUP计算结果为12 var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32} var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768} 最后分配的大小为 size_to_class8[12] == 9 class_to_size[9] == 112 表示分配的内存大小是112,假设et.size为2,则实际cap为112/2=56 大对象 如果是需要分配大对象,就需要进行其他处理了。 首先假设需要的size非常大,加上一个新的_PageSize(8k)溢出了,那就没办法了,申请多少就是多少吧。 否则,就按照_PageSize递增的方式来分配内存 return alignUp(size, _PageSize) ... func alignUp(n, a uintptr) uintptr { return (n + a - 1) &^ (a - 1) } 又是一个二进制运算,他的含义是,把size提到_PageSize的倍数。 比如,申请的size是2.5 * _PageSize ,那么实际分配 3 *_PageSize。 分配内存 终于计算好了分配多少内存,接下来就是执行具体分配内存的策略了。 由于这块是内存管理的范畴了,我们就直接跳过了。 结论 通过上面大量的分析,我们终于知道了go到底如何扩容的数组了 首先小于1024,则double 大于1024,则按照1.25的倍率增加 根据请求的内存大小,进行内存对齐 cap就是内存对齐的大小/元素大小 参考 append implemention1 append implemention2 golang 神奇函数 golang内存堆栈管理 golang内存管理 golang内存分配 ## Publication Information - [hundredwz](https://paragraph.com/@hundredwz/): Publication homepage - [All Posts](https://paragraph.com/@hundredwz/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@hundredwz): Subscribe to updates