切片扩容大法
Q:你先简单介绍一下自己吧
A:嘛哩嘛哩哄,嘛哩嘛哩哄,嘛哩嘛哩哄
Q:你说这么多嘛哩嘛哩哄,那你了解数组吗?
A:数组是具有相同类型且长度固定的数据结构,由于长度不可改变,在实际中使用较少,一般用切片来代替使用。
Q:你刚刚说到切片,切片和数组有什么相同点和不同点吗?
A:相同点是他们都是只能储存相同类型的数据结构,切片的底层数据也是基于数组的,不同点是切片是可以改变长度的,能自动扩容。
Q:嗯,那切片的底层是怎么组成的呢,它的扩容规则你清楚吗?
A:切片由三部分组成,分别是切片的长度,切片的容量,切片的数据(指向底层数组的指针),至于扩容规则可就难说了。
Q:难说也是要说的,你搞快点。
A:切片扩容的基本规则是:当切片append的时候(会调用下面的函数),新切片的必须容量大于老切片容量的两倍的,那么新切片的容量就是新切片的必须容量,反之,如果老切片的长度小于1024,那么新切片的容量就等于老切片的容量的两倍,如果老切片大于等于1024,则新切片容量等于老切片容量的1.25倍,这是切片的预估容量。
// runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
//et 表示slice的一个元素,old 表示旧的slice cap 表示新切片必须的容量
...
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 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
}
}
}
...
}
根据以上规则 ,一下切片扩容必须的切片容量为 5 ,5 > 2*2,所以打印出来应该是len=5, cap=5
package main
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
,原来容量计算完了后还要考虑到内存的高效利用,进行内存对齐,则会调用这个函数
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize {
if size <= smallSizeMax-8 {
return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
} else {
return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])
}
}
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}
size 表示新切片需要的内存大小 我们传入的int 类型,每个占用8字节(可以调用unsafe.Sizeof()
函数查看占用的大小),一共5个 所以是40,size小于_MaxSmallSize 并且小于 smallSizeMax-8 ,那么使用通用公式(size+smallSizeDiv-1)/smallSizeDiv
计算得到 5,
然后到size_to_class8
找到第五号元素 为 4
,再从 class_to_size
找到 第四号元素 为 48,这就是新切片占用的内存大小,每个int 占用 8 字节,所以最终切片的容量为 6 。所以说切片的扩容有它基本的扩容规则,在规则之后还要考虑内存对齐,这就代表不同数据类型的切片扩容的容量大小是会不一致。
Q:好,那我们进入下一个问题
本作品采用《CC 协议》,转载必须注明作者和本文链接
Go1.18不再以1024为临界点,而是设定了一个值为256的threshold,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量+3256)/4; 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容; 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍; 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3threshold)/4。
在1.18中,优化了切片扩容的策略,让底层数组大小的增长更加平滑: 通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从2到1.25的突变,该commit作者给出了几种原始容量下对应的“扩容系数” 可以看到,Go1.18的扩容策略中,随着容量的增大,其扩容系数是越来越小的,可以更好地节省内存。 我们可以试着求一个极限,当oldcap远大于256的时候,扩容系数将会变成1.25。