Golang slice扩容详解

190 阅读2分钟

本文写自golang1.18.3版本

slice的扩容,通常为2x,当容量较大时,会调整为1.25x,但实际上到底是怎么调整的呢?下面我们通过golang的源码来理解。

例子一

func main() {
  a := make([]int, 4)
  fmt.Printf("len(a): %v, cap(a): %v\n", len(a), cap(a))
  // len(a): 4, cap(a): 4
  a = append(a, 1)
  fmt.Printf("len(a): %v, cap(a): %v\n", len(a), cap(a))
  // len(a): 5, cap(a): 8
}

可以看到,slice的容量在不足时进行了2x,源码中体现为

image-20230224103547795

其中,et表示slice的元素的类型信息,old表示扩容前的slice,cap表示至少需要的容量(在这里是4+1=5)。newcap代表申请的新slice的容量,但申请容量和实际得到的容量会由于Golang的内存分配的原因而有所不同,下文将详细介绍。在例子一的情况下,cap为5,old.cap为4,那么newcap为8。

例子二

func main() {
  a := make([]int, 512)
  fmt.Printf("len(a): %v, cap(a): %v\n", len(a), cap(a))
  // len(a): 512, cap(a): 512
  a = append(a, 1)
  fmt.Printf("len(a): %v, cap(a): %v\n", len(a), cap(a))
  // len(a): 513, cap(a): 848
}

同理,cap为512+1=513,old.cap为512,newcap = 1.25 x 512 + 0.75 x 256 = 832

例子三

func main() {
  a := make([]int, 4)
  fmt.Printf("len(a): %v, cap(a): %v", len(a), cap(a))
  // len(a): 4, cap(a): 4
  a = append(a, []int{1, 2, 3, 4, 5}...)
  fmt.Printf("len(a): %v, cap(a): %v", len(a), cap(a))
  // len(a): 9, cap(a): 10
}

cap为4+5=9,old.cap为4,newcap = cap = 9

接下来,介绍字节对齐的部分,直接上源码(我去除了大部分的源码,但保留了一些,这样你才知道你看的是源码)

image-20230224110422200

前文我们提到过et为slice的元素的类型信息,其中et.size为元素的大小,同时goarch.PtrSize为当前平台的指针类型的长度,而对于64位机器平台,int类型和指针类型都为8B,因此我们仅介绍et.size == goarch.PtrSize这种情况,其他同理。

以例子三为例,newcap = 9, et.size = 8, 因此capmem = roundupsize(72),老规矩,直接上源码。

image-20230224112910593

roundupsize(72) => divRoundUp(72, 8) = 79 / 8 = 9 => size_to_class8[9] = 7 => class_to_size[7] = 80

因此newcap = int(80 / 8) = 10。

例子二:

roundupsize(832 x 8) => divRoundUp(6656 - 1024, 128) = 5759 / 128 = 44 => size_to_class128[44] = 49 => class_to_size[49] = 6784

因此newcap = int(6784 / 8) = 848。

总结

slice的动态调整,不仅仅只是根据当前大小来决定是2x还是1.25x,同时也和Golang的内存分配相关。

参考

  1. 如何编译调试Go runtime源码
  2. Golang's memory allocation