1. 最简单的扩容机制——追加一个元素且在 1024 字节以内
- 如果新的大小在 cap 大小以内自然不需要考虑扩容问题
- 如果超出 cap 则 cap 翻倍
2. 追加一个元素超过 1024 字节
- cap 超过 1024 字节,扩容则变为 1.25 倍而不再是 2 倍(当然这只是之前版本,现在 go 的切片扩容已经不会直接由 2 倍 降低 为 1.25 倍,而是更平滑的降低)
3. 复杂扩容——追加多个元素
- 当追加的最终元素大小,超过了 cap 的二倍时,则扩容为原大小直接加追加大小,如再两个元素后再加 3 个元素,新的 cap 则为 5
以上三种规则也是我们大部分人所认为的常识
OK 到这里为止 go 的切片扩容机制真的只是如此吗 ?当然不是如果你去尝试我所说的第 3 个总结的话,自然就发现问题了,如下
package main
import "fmt"
func main() {
e := []int32{1, 2}
fmt.Println("cap of e before:", cap(e))
e = append(e, 4, 5, 6)
fmt.Println("cap of e after:", cap(e))
}
输出:
很显然输出并非我们所想的
新
我们所总结的规则来自于 go 扩容机制 runtime/slice
包中的 growslice
源代码,只是我们所总结的只是 go 扩容机制的第一步,也就是他所期望的 cap 大小,真正分配时又会有其他的影响因素,例如在分配时 span class ,也就是 go 内存分配机制的 span 大小种类,
例如我们上面的代码, 我们期望分到 5 个元素的长度,其中 int 类型占 8 个字节,也就是分得 40 个字节的内存,ok ,那我们 span 的种类有哪些呢,如下图
67中不同大小的span代码注释如下(版本1.11):
// 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%
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
// 11 160 8192 51 32 9.73%
// 12 176 8192 46 96 9.59%
// 13 192 8192 42 128 9.25%
// 14 208 8192 39 80 8.12%
// 15 224 8192 36 128 8.15%
// 16 240 8192 34 32 6.62%
// 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%
// 26 576 8192 14 128 12.33%
// 27 640 8192 12 512 15.48%
// 28 704 8192 11 448 13.93%
// 29 768 8192 10 512 13.94%
// 30 896 8192 9 128 15.52%
// 31 1024 8192 8 0 12.40%
// 32 1152 8192 7 128 12.41%
// 33 1280 8192 6 512 15.55%
// 34 1408 16384 11 896 14.00%
// 35 1536 8192 5 512 14.00%
// 36 1792 16384 9 256 15.57%
// 37 2048 8192 4 0 12.45%
// 38 2304 16384 7 256 12.46%
// 39 2688 8192 3 128 15.59%
// 40 3072 24576 8 0 12.47%
// 41 3200 16384 5 384 6.22%
// 42 3456 24576 7 384 8.83%
// 43 4096 8192 2 0 15.60%
// 44 4864 24576 5 256 16.65%
// 45 5376 16384 3 256 10.92%
// 46 6144 24576 4 0 12.48%
// 47 6528 32768 5 128 6.23%
// 48 6784 40960 6 256 4.36%
// 49 6912 49152 7 768 3.37%
// 50 8192 8192 1 0 15.61%
// 51 9472 57344 6 512 14.28%
// 52 9728 49152 5 512 3.64%
// 53 10240 40960 4 0 4.99%
// 54 10880 32768 3 128 6.24%
// 55 12288 24576 2 0 11.45%
// 56 13568 40960 3 256 9.99%
// 57 14336 57344 4 0 5.35%
// 58 16384 16384 1 0 12.49%
// 59 18432 73728 4 0 11.11%
// 60 19072 57344 3 128 3.57%
// 61 20480 40960 2 0 6.87%
// 62 21760 65536 3 256 6.25%
// 63 24576 24576 1 0 11.45%
// 64 27264 81920 3 128 10.00%
// 65 28672 57344 2 0 4.91%
// 66 32768 32768 1 0 12.50%
很显然我们没有 40 字节大小的 span ,所以向上取整,得到一个 48 字节大小的 span ,也就是最终分得 cap 长度为 6
到这里就结束了吗,当然不是,如果你好奇的话,你继续扩大追加的元素个数,你会发现扩容机制再次发生改变,也就是说,span class 的大小也只是其中的一个影响因素
到这里为止,就直接公布我所认为的扩容机制应该如何处理:
答案就是:去纠结它的扩容确切值并没什么必要
理由:在扩容的容量确定上,相对比较复杂,它与CPU位数、元素大小、是否包含指针、追加个数
等都有关系。当我们看完扩容源码逻辑后,发现去纠结它的扩容确切值并没什么必要。
在实际使用中,如果能够确定切片的容量范围,比较合适的做法是:切片初始化时就分配足够的容量空间,在append追加操作时,就不用再考虑扩容带来的性能损耗问题。