go 的切片扩容机制

**题目来源:**小米

Go 1.18版本之前

答案1:

扩容是为切片分配新的内存空间并复制原切片中元素的过程。在 go 语言的切片中,扩容的过程是:估计大致容量 -> 确定容量 -> 覆盖原切片 -> 完成扩容。
先确定新的切片大致容量而分配内存空间,根据该切片当前容量选择不同的策略:

  • 如果期望容量大于当前容量的两倍,就会使用期望容量
  • 如果当前切片的长度小于 1024,容量就会翻倍
  • 如果当前切片的长达大于 1024,每次扩容 25% 的容量,直到新容量大于期望容量。
    估计大致容量后,开始确定容量,根据切片中的元素大小对齐内存情况来确定容量。当元素所占字节大小为 1、2、8 的倍数时,会通过 runtime.roundupsize 函数来确定待申请的内存,该函数会从一个数组中获取整数,使用这个数组中的元素可以提高内存分配效率并减少碎片,这个数组叫做 NumSizeClasses 。

扩展:切片的扩容与追加元素息息相关。是否扩容是由中间代码生成阶段的 cmd/compile/internal/gc.state.append 方法的返回值决定的,这个方法的返回值是一个新切片,如果这个新切片不赋值回原变量,那么 go 会获取这个切片的数组指针、大小和容量,判断追加元素后切片大小是否大于容量,如果大于,会调用 runtime.growslice 对切片进行扩容并将新元素依次加入新切片。如果方法返回的新切片,赋值回原变量,那么会以另外一种方式判断是否需要扩容,当然,扩容还是通过 runtime.growslice。不过它们的区别在新切片是否会赋值回原变量,如果覆盖原变量,就无须担心切片发生复制而影响性能。

Go 1.18版本切片扩容

Go1.18不再以1024为临界点,而是设定了一个值为256的threshold,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量+3*256)/4;

  • 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
  • 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
  • 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。

公式如下:

45.go 的切片扩容机制 - 图1