go 的切片扩容

在uber 的go语言编码规范中有这么一条,2.48s 和0.21s 的差距还是很惊人了,我很好奇,why?

20211101012619128

切片在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容量的扩容规则:当原slicecap小于1024时,新slicecap变为原来的2倍;原slicecap大于1024时,新slice变为原来的1.25倍,按照这个规则扩充后,还会进行内存对齐操作。

回到开头,为什么确定切片容量的程序效率更高?因为他省去了扩容的步骤。

扩展:内存对齐的目的

image-20211101012421747

假设CPU的内存读写单位为4字节

在内存对齐和非对齐情况下,读取变量a都仅需要读取一次。

在内存对齐情况下,如果要读取变量b,则仅需要读取1次,即第二部分(4-7);而非对齐情况下,则需要读取2次,即第一部分(0-3)取后3个字节,第二部分取前1个字节,然后用或操作拼接成变量b。

因此,内存对齐在某些情况下可以减少内存的读取次数,提高性能,是一种空间换时间的策略。