大约2021年8月份,go社区对切片容量增长的方式进行了一次调整。具体讨论可见:https://groups.google.com/g/golang-nuts/c/UaVlMQ8Nz3o

1. 之前的增长规则

先看源码

runtime/slice.go

func growslice(et *_type, old slice, cap int) slice {
	// 省略部分条件检查
    // ...

	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.cap < 1024 {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

    // 对切片进行内存对齐,可能会调整切片容量大小
    // ...

	return slice{p, old.len, newcap}
}

从代码中可以看出,当cap < 1024时,切片容量增长2倍,当cap > 1024时增长1.25倍,当然,在经过内存对齐等操作后,切片的长度可能不会严格等于原来的2倍或1.25倍。

存在的问题:

  1. not monotonically increasing (不平滑)
x1 := make([]int, 897)
x2 := make([]int, 1024)
y := make([]int, 100)
println(cap(append(x1, y...))) // 2048
println(cap(append(x2, y...))) // 1280

对于append(x1, y...), 进行growslice,传入的cap为所需的最小容量,为:cap(x1) + cap(y) = 997。 此时newcap为897, doublecap为897×2 = 1794。由于old.cap = 897 < 1024, 所以newcap = doublecap,为1794,再经过内存对齐后,最终的容量为2048.

对于append(x2, y...), 进行growslice,传入的cap为1124,此时newcap为1024, doublecap为1024×2 = 2048. 由于cap < doublecapnewcap< cap,因此newcap += newcap / 4 = 1280.经过内存对齐后,仍为1280.。

这样就产生了一个问题,在一定条件下,如果cap(A) < cap(B), 但二者都append一个相同长度的C后,反而cap(A) > cap(B)。

  1. not symmetrical (不对称)
x := make([]int, 98)
y := make([]int, 666)
println(cap(append(x, y...))) // 768
println(cap(append(y, x...))) // 1360

2. 调整之后

源码:

func growslice(et *_type, old slice, cap int) slice {

    // 省略部分条件检查
    // ...

	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
			}
		}
	}

	// 对切片进行内存对齐,可能会调整切片容量大小
    // ...

	return slice{p, old.len, newcap}
}

进行的调整:

  • 增加了threshold = 256
  • 增长策略不同的阈值从1024变为256
  • 当切片容量大于阈值时,增长策略变化

原来的问题:

  1. 增长不平滑
x1 := make([]int, 897)
x2 := make([]int, 1024)
y := make([]int, 100)
println(cap(append(x1, y...))) // 1360
println(cap(append(x2, y...))) // 1536
  1. 增长不对称
x := make([]int, 98)
y := make([]int, 666)
println(cap(append(x, y...))) // 768
println(cap(append(y, x...))) // 1024

增长不平滑的问题得到了较好的解决,同时不对称的问题也得到了缓解。