每个语言都有自己的动态数组,甚至有的语言数组本身就是动态的,这里的动态是指长度可变,从而可以更灵活的应对各种情况。Go 语言也不例外,它提供了常规数组,也提供了动态数组 —— 切片。
Struct
切片的结构很简单,只有三个字段,指向底层数组的指针,还有底层数组的容量,以及当前长度。
type slice struct {array unsafe.Pointerlen intcap int}
代码 1:slice struct
图 1:slice struct
Go 语言支持通过内置函数 len(slice) 和 cap(slice) 获取切片长度以及容量,这两个函数在编译后会自动变为直接取地址,所以不用担心其性能。
Append
追加是切片最常用的操作,通过编译之后,append 等同于下面代码,对于容量足够的切片没什么好说的,就是直接对 len 以及后面的元素赋值就可以了。L5 ~ L9 是在新的切片长度大于底层数组容量时,将底层数组扩大。
// slice = append(slice, 1, 2, 3)a := &sliceptr, len, cap := slicenewlen := len + 3if uint(newlen) > uint(cap) {newptr, len, newcap = growslice(slice, newlen)vardef(a)*a.cap = newcap*a.ptr = newptr}newlen = len + 3*a.len = newlen*(ptr+len) = 1*(ptr+len+1) = 2*(ptr+len+2) = 3
代码 2:append
图 2:append
这里可以分三种情况,分别是:
- new_cap > 2 * old_cap: cap = new_cap
- new_cap < 2 old_cap && old_cap < 1024: cap = 2 old_cap
- new_cap < 2 * old_cap && old_cap > 1024: cap = old_cap + old_cap / 4(until cap > new_cap)
```go
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
} else {newcap = cap
}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}}
**代码 3:growslice newcap**<br />**图 3:growslice newcap**```govar overflow boolvar 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:lenmem = uintptr(old.len)newlenmem = uintptr(cap)capmem = roundupsize(uintptr(newcap))overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)case et.size == sys.PtrSize:lenmem = uintptr(old.len) * sys.PtrSizenewlenmem = uintptr(cap) * sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)overflow = uintptr(newcap) > maxAlloc/sys.PtrSizenewcap = int(capmem / sys.PtrSize)case isPowerOfTwo(et.size):var shift uintptrif 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) << shiftnewlenmem = uintptr(cap) << shiftcapmem = roundupsize(uintptr(newcap) << shift)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)default:lenmem = uintptr(old.len) * et.sizenewlenmem = uintptr(cap) * et.sizecapmem, overflow = math.MulUintptr(et.size, uintptr(newcap))capmem = roundupsize(capmem)newcap = int(capmem / et.size)}
代码 4:multiplication optimization
在计算真正需要分配的内存的大小,这里是根据 size 是否为 2 的倍数,进行了一些优化
- size = 1: 不需要处理
- size = sys.PtrSIze:这里看上去是直接相乘,但是因为 sys.PtrSize 是常数,所以编译器会优化为左移
- size 是 2 的倍数:左移优化
- size 不是 2 的倍数:直接相乘
另外还需要注意一下 roundupsize 函数,这个函数就是将分配的内存向上取整,这和 Go 语言的内存分配策略有关,可以减少内存碎片。
