go 的切片扩容
在uber 的go语言编码规范中有这么一条,2.48s 和0.21s 的差距还是很惊人了,我很好奇,why?
切片在append
的时候可能会自动扩容,看一下相关源码。
slice
type slice struct {
array unsafe.Pointer
len int
cap int
}
func makeslice(et *_type, len, cap int) unsafe.Pointer {
// 1. 计算需要申请的容量,并判断是否内存溢出
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
// 2. 内存溢出原因
if overflow || mem > maxAlloc || len < 0 || len > cap {
// 计算slice所需内存通过MulUintptr来实现的
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
// 3. 进行内存分配
return mallocgc(mem, et, true)
}
func growslice(et *_type, old slice, cap int) slice {
...
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
// 当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量
if old.cap < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/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
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
...
}
package runtime
// 内存对齐的过程,为了避免造成过多的内存碎片
func roundupsize(size uintptr) uintptr {
// size=1600*8=12800<32768
if size < _MaxSmallSize {
// 12800<=0
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]])//size_to_class128[92]= 56
//class_to_size[56]=13568
//13568/8=1696
}
}
if size+_PageSize < size {
return size
}
return round(size, _PageSize)
}
const _MaxSmallSize = 32768
const smallSizeDiv = 8
const smallSizeMax = 1024
const largeSizeDiv = 128
总结
slice
容量的扩容规则:当原slice
的cap
小于1024
时,新slice
的cap
变为原来的2倍;原slice
的cap
大于1024
时,新slice
变为原来的1.25
倍,按照这个规则扩充后,还会进行内存对齐操作。
回到开头,为什么确定切片容量的程序效率更高?因为他省去了扩容的步骤。
扩展:内存对齐的目的
假设CPU的内存读写单位为4字节
在内存对齐和非对齐情况下,读取变量a都仅需要读取一次。
在内存对齐情况下,如果要读取变量b,则仅需要读取1次,即第二部分(4-7);而非对齐情况下,则需要读取2次,即第一部分(0-3)取后3个字节,第二部分取前1个字节,然后用或操作拼接成变量b。
因此,内存对齐在某些情况下可以减少内存的读取次数,提高性能,是一种空间换时间的策略。