golang切片扩容机制

960 阅读4分钟

前言

golang的切片扩容机制是golang面试者绕不开的一扇大门,无论在面试提问,或者面试情景上都绕不开它,今天就说说我理解下的切片扩容。

golang的扩容机制:在go1.18之前有一个临界值为1024,小于1024的时候,切片先两倍扩容,如果两倍扩容后的容量还是不够,就直接以切片需要的容量作为容量。

在go1.18之后,临界值换成了256,小于256和前面相同,大于256公式变为(oldcap+3*256)/4这个公式的值随着oldcap的越来越大,从2一直接近1.25,相对于1.18之前可以更平滑的过渡。

上面就是切片扩容的基本规则机制,但还有一个小的规则,下面就来看看

发现问题

看下面一行代码

image.png

运行结果如下

image.png

出现上面的结果原因是,刚开始切片容量为1,两倍扩容之后,变为2,但是2不够,所以变为所需要的容量3,后面在加4个数字,继续扩容,2倍之后是6,但是6不够应该变为需要的容量,应该为7,为什么是8呢

解决问题

先贴一下源代码,下面第一段代码是扩容机制的基础代码(1.18之前)

image.png

下面是造成上面原因的代码

image.png

下面是1.18之后的基础扩容机制

image.png

从上面代码分析之后,造成7变成8的原因就在这个roundupsize函数上面,它有一个计算公式

capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)

其中 ptrSize 在 64 位机器下的大小为 8。 而此时 newcap 的值为 7,所以传入到 roundupsize 函数内部的值为 7 * 8 = 56 。接着看看 roundupsize 的内部:

func roundupsize(size uintptr) uintptr {
	if size < _MaxSmallSize {
		if size <= smallSizeMax-8 {
			return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
		} else {
			//……
		}
	}
    //……
}
const _MaxSmallSize = 32768
const smallSizeMax = 1024
const smallSizeDiv = 8

var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 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, 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, 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, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31}

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}

roundupsize 的返回值为 uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]),而:

  1. (size+smallSizeDiv-1)/smallSizeDiv = (56 + 8 - 1) / 8 = 7
  2. size_to_class8[7] = 5
  3. class_to_size[5] = 64 所以 roundopsize 的返回值为 64 ,newcap = int(capmem / ptrSize) = int(64 / 8) = 8 ,所以最终原切片的容量扩充到了 8。

总结

如果以后遭遇情景题我们按照这个公式算的话很费时间,所以我总结了一下规则,就是如果遇见2倍扩容之后不够需要直接用它所需要的容量的情况,如果是1和3,它的容量可以是1和3,但如果是5,7,9,11等其他的奇数,全部取+1得到离他最近的偶数,因为这个值与class_to_size有关,class_to_size全部存的是8的倍数,把class_to_size里面的值全部除以8,结果为:0,1,2,3,4,6,8,10...........和咱们的规律非常吻合