Go 语言切片的容量是如何增长的?
Go 语言切片的容量是如何增长的?
回答重点
在 Go 语言中,切片的容量是一种动态增长的机制。当切片的长度达到或超过容量时,Go 语言会自动扩展其底层数组的容量,一般由append触发。切片容量增长(growslice)的具体规则在不同版本的规则不同。
对于 go1.18 之前来说:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 的话, growslice 时就会将容量翻倍;
- 如果当前切片的长度大于 1024 的话,growslice 时会每次增加 25% 的容量,直到新容量大于期望容量。
对于 go1.18 之后来说,减小了倍增阈值,但是在后续25%的幅度增加的时候把阈值作为基准的一部分,来避免扩容次数过多的问题:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于阈值 256的话,growslice 时就会将容量翻倍;
- 如果当前切片的长度大于等于256,就会每次增加
(newcap + 3*threshold)/4的容量,直到新容量大于期望容量。
此外,在扩容之后还会进行一步roundupsize,这一步主要是靠内存对齐的优化,来计算出最终的容量。
扩展知识
要深入了解 Go 语言切片的容量增长机制,得从它的底层实现说起。
1)1.18之前和之后的源码实现
1.18之前,可以发现在大于 1024 阈值之后,是每次按照1/4的容量幅度增长的。
1 | func growslice(et *_type, old slice, cap int) slice { |
go 1.18 之后,可以看到在大于阈值 256 之后,每次是按照(newcap + 3*threshold)/4的幅度增长。
1 | func growslice(et *_type, old slice, cap int) slice { |
1)切片与底层数组的关系:
- 切片本质上是一个对底层数组的抽象。一个切片包含三个部分:指向底层数组的指针、切片的长度和切片的容量。
- 切片是动态数组,它的大小并非固定不变,而是可以根据需要动态扩展。
2)切片容量增大的过程:
- 当我们向切片中追加元素,如果已有的容量不足以容纳新的元素,Go 语言会自动分配一个更大的底层数组,并将旧的元素复制到新的数组中。
- 在容量小于 1024 时,新的容量至少是旧容量的两倍。而当容量达到或超过 1024 时,增长因子减小为 1.25。
3)示例代码:
- 这段代码展示了切片容量的自动增长。
1 | package main |
4)什么是内存分配和复制?
- 当切片的容量扩展时,Go 运行时会新分配一块更大的内存空间,这涉及到内存分配操作。
- 然后,运行时将旧的底层数组的内容复制到新的内存空间。复制操作虽然是透明的,但是它的开销不容忽视,尤其是在大规模数组操作时。因此,为了减少频繁的容量扩展,通常我们可以在初始化切片时就指定一个合理的容量。
5)Go 语言优化措施:
- Go 语言中的切片设计考虑了性能和灵活性,它采用了倍增和低倍增的机制来实现容量的自动成长。
- 这种设计使得切片既能在小规模数据处理时保持高效,又能在处理大规模数据时避免频繁的内存分配和数据复制开销。
Comments