1.Golang1.16的扩容
谈到Golang slice的扩容策略,大家可能脱口而出的就是:
1024容量下2倍扩容。
1024以上1.25倍扩容。
下面来看一个例子: Go1.16
func main() {
s1 := make([]int, 1)
oldLen, oldCap := len(s1), cap(s1)
s1 = append(s1, 1)
fmt.Println(oldLen, len(s1), oldCap, cap(s1)) // 1 2 1 2
s2 := make([]byte, 1)
oldLen, oldCap = len(s2), cap(s2)
s2 = append(s2, '1')
fmt.Println(oldLen, len(s2), oldCap, cap(s2)) // 1 2 1 8
}
我们在初始化1个容量的int数组,发生扩容时,符合我们的预期,进行了2倍扩容。
但是在初始化1个容量的byte数组,发生扩容时,扩容容量变为8,发生了8倍扩容。
造成这一现象的原因是什么呢?
我们来看 slice 扩容的源码:
go/src/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
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
switch {
case et.size == 1:
newcap = int(capmem)
case et.size == sys.PtrSize:
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
newcap = int(capmem >> shift)
default:
newcap = int(capmem / et.size)
}
// ...
}
从源码我们可以看到:
第一部分确实按照我们的预期,< 1024 时进行2倍扩容,大于等于1024时,进行1.25倍扩容。
但是除了第一部分外,还有第二部分的内容。会基于切片存储的数据类型,以及当前的切片容量,进行内存对齐,计算返回内存读取最优的长度作为容量。内存对齐核心方法:roundupsize()。感兴趣的话可以再去了解(🔍揭秘Golang内存分配:slice 扩容策略全解析!)
所以,再回答slice扩容机制时,除了考虑上述两点外,谈到内存对齐,也是很必要的点!
2.Golang1.18的扩容
对于Golang1.18版本,出于平衡内存使用效率和性能,扩容策略有了比较大的更改。
查看源码如下:Go1.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
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
}
}
}
// ...
}
总结:
原切片容量小于 256:如果原切片容量小于 256,新容量会直接翻倍。例如原切片容量为 128,扩容后的新容量就是 256。
原切片容量大于等于 256:当原切片容量大于等于 256 时,扩容会采用更复杂的策略。新容量会在原容量的基础上增加一个 0 到 25% 之间的 “额外” 容量,具体增加的比例会根据情况动态调整。这种策略旨在平衡内存使用效率和性能。比如原切片容量为 512,扩容后的新容量可能会大于 512 但小于 640(512 + 512 * 25%)。
内存对齐:同1.16一样,1.18也会进行内存对齐,以期望最优的内存处理效率。
评论