切片扩容
在 1.18 版本前,切片扩容,在容量小于1024时,以2倍大小扩容。超过1024后,以1.25倍扩容。在扩容后切片的基础上,会根据长度和容量进行 roundupsize 。
在1.18版本后,接下来看一下源码如下:
func growslice(et *_type, old slice, cap int) slice {
// ......
newcap := old.cap
doublecap := newcap + newcap //双倍扩容(原容量的两倍)
if cap > doublecap { //如果所需容量大于 两倍扩容,则直接扩容到所需容量
newcap = cap
} else {
const threshold = 256 //这里设置了一个 阈值 -- 256
if old.cap < threshold { //如果旧容量 小于 256,则两倍扩容
newcap = doublecap
} else {
// 检查 0 < newcap 以检测溢出并防止无限循环。
for 0 < newcap && newcap 0 并且 原容量 小于 所需容量
// 从小片的增长2x过渡到大片的增长1.25x。这个公式给出了两者之间的平滑过渡。
newcap += (newcap + 3*threshold) / 4
//新容量是 = 1.25 原容量 + 3/4 阈值 (192)
}
//当newcap计算溢出时,将newcap设置为请求的上限。
if newcap -1] cap = 0 | after append 0 cap = 1
[0 -> 0] cap = 1 | after append 1 cap = 2
[0 -> 1] cap = 2 | after append 2 cap = 4
[0 -> 3] cap = 4 | after append 4 cap = 8
[0 -> 7] cap = 8 | after append 8 cap = 16
[0 -> 15] cap = 16 | after append 16 cap = 32
[0 -> 31] cap = 32 | after append 32 cap = 64
[0 -> 63] cap = 64 | after append 64 cap = 128
[0 -> 127] cap = 128 | after append 128 cap = 256
[0 -> 255] cap = 256 | after append 256 cap = 512
[0 -> 511] cap = 512 | after append 512 cap = 1024
[0 -> 1023] cap = 1024 | after append 1024 cap = 1280
[0 -> 1279] cap = 1280 | after append 1280 cap = 1696
[0 -> 1695] cap = 1696 | after append 1696 cap = 2304
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
运行结果(1.18 版本):
[0 -> -1] cap = 0 | after append 0 cap = 1
[0 -> 0] cap = 1 | after append 1 cap = 2
[0 -> 1] cap = 2 | after append 2 cap = 4
[0 -> 3] cap = 4 | after append 4 cap = 8
[0 -> 7] cap = 8 | after append 8 cap = 16
[0 -> 15] cap = 16 | after append 16 cap = 32
[0 -> 31] cap = 32 | after append 32 cap = 64
[0 -> 63] cap = 64 | after append 64 cap = 128
[0 -> 127] cap = 128 | after append 128 cap = 256
[0 -> 255] cap = 256 | after append 256 cap = 512
[0 -> 511] cap = 512 | after append 512 cap = 848
[0 -> 847] cap = 848 | after append 848 cap = 1280
[0 -> 1279] cap = 1280 | after append 1280 cap = 1792
[0 -> 1791] cap = 1792 | after append 1792 cap = 2560
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
newcap + 3*threshold
,直到新容量大于期望容量;内存对齐
但是,后半部分还对 newcap
作了一个内存对齐
,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于
按照前半部分生成的newcap
。
之后,向 Go 内存管理器申请内存,将老 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中。
最后,向 growslice
函数调用者返回一个新的 slice,这个 slice 的长度并没有变化,而容量却增大了。
测试代码
import "fmt"
func main() {
s := []int{1,2}
s = append(s,4,5,6)
fmt.Printf("len=%d, cap=%d",len(s),cap(s))
}
输出
len=5, cap=6
根据Go语言中切片的扩容机制,当切片容量不足以容纳额外的元素时,它会自动进行扩容。在这种情况下,切片的容量会根据需要自动增长,通常会以原来容量的2倍进行扩容。
根据您的代码,初始切片s的容量为2,添加了3个元素后,长度变为5。实际上,在这种情况下,切片的容量不会立即扩大到8,而是继续保持为6。这是因为Go语言的切片扩容机制会优化以减少内存的浪费。扩容时,切片会选择一个合适的容量,使得容量尽可能靠近但大于所需的最小容量。
源码分析
func growslice(et *_type, old slice, cap int) slice {
// ……
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
// ……
}
// ……
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}
capmem = roundupsize(uintptr(newcap) * ptrSize)
:根据 newcap
和指针的大小计算需要分配的内存大小。
newcap = int(capmem / ptrSize)
:通过将内存大小除以指针大小,将新的容量 newcap
转换为整数。
参考文章:
- go.dev/doc/go1.18
- go.dev/blog/slices
- go.dev/blog/slices…
- golang.design/go-question…
- draveness.me/golang/docs…