go slice 扩容深度分析(1.18版本的golang已经改变之前的分配方式)

1、切片扩容常规操作

如果超过容量,会重新扩一个内存,创建新的数组,并把指针指向这个数组,容量如果不超过1024会扩一倍,如果容量超过1024扩容量会每次增加25%。

package main

import (
    "fmt"
)

func main() {
    a := []int{}
    for i := 0; i < 33; i++ {
        a = append(a, i)
        fmt.Print(cap(a), " ")
    }
}

结果:

1 2 4 4 8 8 8 8 16 16 16 16 16 16 16 16 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 64

2、切片扩容非常规操作(一次插入多个数字)

package main

import "fmt"

func main() {
    b := []int{23, 51}
    b = append(b, 4, 5, 6)
    fmt.Println("cap of b is ",cap(b))
}

猜猜结果是多少?

cap of b is  6

想知道结果我们翻看golang 的源码,我的版本是1.18beta1,老版本的扩容代码和新版本有些许区别

代码位置:src/runtime/slice.go

//老版本
newcap := old.cap
if newcap+newcap < cap {
    newcap = cap
} else {
    for {
        if old.len < 1024 {
            newcap += newcap
        } else {
            newcap += newcap / 4
        }
        if newcap >= cap {
            break
        }
    }
}

// 1.18beta版
newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
  • 第一行的 old.cap:扩容前的容量,对于此例,就是 2
  • 第二行的 cap:扩容前容量加上扩容的元素数量,对于此例,就是 2+3

新版本的和老版本的区别在于新版本把阈值设为256,并且改变了增长公式

newcap += (newcap + 3*threshold) / 4,使之增长更加平滑

看代码那最终容量不是5吗?

其实不然,newcap是计算出扩容后的预估容量,并不是最终的容量,要计算最终的容量,还需要参考另一个维度,也就是内存分配。

关于内存管理模块的代码,在 runtime/sizeclasses.go

// class  bytes/obj  bytes/span  objects  tail waste  max waste
//     1          8        8192     1024           0     87.50%
//     2         16        8192      512           0     43.75%
//     3         32        8192      256           0     46.88%
//     4         48        8192      170          32     31.52%
...
//    17        256        8192       32           0      5.86%
//    18        288        8192       28         128     12.16%
//    19        320        8192       25         192     11.80%
//    20        352        8192       23          96      9.88%
//    21        384        8192       21         128      9.51%
//    22        416        8192       19         288     10.71%
//    23        448        8192       18         128      8.37%
//    24        480        8192       17          32      6.82%
//    25        512        8192       16           0      6.05%
...
//    66      32768       32768        1           0     12.50%

5个int是40字节,向上取整是48字节所以cap是6的原因。