// growslice handles slice growth during append. // It is passed the slice element type, the old slice, and the desired new minimum capacity, // and it returns a new slice with at least that capacity, with the old data // copied into it. // The new slice's length is set to the old slice's length, // NOT to the new requested capacity. // This is for codegen convenience. The old slice's length is used immediately // to calculate where to write new values during an append. // TODO: When the old backend is gone, reconsider this decision. // The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
// et 为 slice 元素的类型,这里是 int 类型 // old 为旧的 slice // cap 为容量,此时等于 len(old.cap)+追加元素个数,这里是 9。 // cap为什么是9?后面有相关源码 funcgrowslice(et *_type, old slice, capint) slice { ...
ifcap < old.cap { panic(errorString("growslice: cap out of range")) }
// 元素类型大小为0,则特殊处理 if et.size == 0 { // append should not create a slice with nil pointer but non-zero len. // We assume that append doesn't need to preserve old.array in this case. return slice{unsafe.Pointer(&zerobase), old.len, cap} }
// cap=old.cap + 追加元素的个数 // // 如果 cap 大于原来2倍旧的容量就是用 cap // // 所理解的:在容量小于1024时,在原基础上2倍扩容。大于1024时,在原基础上1/4的扩容 // 也正是源于下面的逻辑 // ifcap > doublecap { newcap = cap } else { const threshold = 256 // 在这里原来1.18之前为小于等于 1024。即: // if old.cap < 1024 // // 1.18之前 threshold 等于 1024 if old.cap < threshold { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for0 < 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 } } }
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 goarch.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: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem)
// divRoundUp returns ceil(n / a). funcdivRoundUp(n, a uintptr)uintptr { // a is generally a power of two. This will get inlined and // the compiler will optimize the division. return (n + a - 1) / a }
③ 在②中 func growslice(et *_type, old slice, cap int) 的 cap 为什么是9? 在编译器的源码 cmd/compile/internal/walk/assign.go 中可以看到蛛丝马迹。
// expand append(l1, l2...) to // init { // s := l1 // n := len(s) + len(l2) // // Compare as uint so growslice can panic on overflow. // if uint(n) > uint(cap(s)) { // s = growslice(s, n) // } // s = s[:n] // memmove(&s[len(l1)], &l2[0], len(l2)*sizeof(T)) // } // s // // l2 is allowed to be a string. funcappendSlice(n *ir.CallExpr, init *ir.Nodes) ir.Node { ......
至此,终于知道了 SliceAppend2() 中,slice 最终容量为10的原因。 同理,也可以通过相同的计算得知 其他几种写法得到 cap 数值的原因。
在源码中还有非常多的细节也可能导致结果有偏差,需要具体案例具体分析
4.小结
对于 slice 的扩容,需要知晓以下几个知识点: 1.在向slice追加元素时,如果 cap 不够,会进行扩容 2.不同go版本的扩容逻辑不同 (go1.18与go1.18之前的版本就不一样),会导致 cap 不一样 3.不同的数据类型有不同扩容逻辑,会导致 cap 不一样 4.相同数据类型,追加不同个数元素,会导致 cap 不一样 5.相同数据类型,批量追加 或者 逐个追加 相同个数 元素,会导致 cap 不一样
假设把 容量为4的int切片追加5个元素 改成 容量为40的int切片追加50个元素,你知道结果吗? 估计需要把所有的 class 等级以及对应等级内存块的大小都背诵下来吧,假设能够背诵下来的话!