go version go1.17.1 darwin/amd64
我們都知道golang中的切片(slice)是基於數組實現,如下圖。
接下來我們就通過源碼來看看slice是怎麼把數組玩出花的
底層數據結構
// go/src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
即array 就是上圖黃色底層數組的地址,len--切片長度,cap--切片容量。也就是說:一個用戶自定義的切片變量的底層數據結構,就是由上面slice的結構
我們通過下面代碼驗證, slice到底是不是用上面結構體保存的
slice1 := make([]int, 1, 10)
ptr := unsafe.Pointer(&slice1)
opt := (*[3]int)(ptr) // 上一步和這一步,可以把任意類型的變量,轉換成指針數組
fmt.Println("slice1: ", opt[0], opt[1], opt[2])
// 輸出 slice1: 824635484112 1 10
可以看出opt[0], opt[1], opt[2]就分別對應array, len, cap。其中array就是底層數組的地址(具體的值在每個人的電腦上會不一樣)。
我們在看看下面的代碼
slice1 := []int{1,2,3,4,5,6,7,8}
fmt.Printf("slice1 addr:%p; data1 addr:%p, data2 addr:%p\n", &slice1, &slice1[0], &slice1[1])
ptr := unsafe.Pointer(&slice1)
opt := (*[3]int)(ptr)
fmt.Println("struct data in slice1: ", opt[0], opt[1], opt[2])
fmt.Println("===")
slice2 := slice1[1:]
fmt.Printf("slice2 addr %p; data2 addr:%p\n", &slice2, &slice2[0])
ptr2 := unsafe.Pointer(&slice2)
opt2 := (*[3]int)(ptr2)
fmt.Println("struct data in slice2: ", opt2[0], opt2[1], opt2[2])
// 輸出
// slice1 addr:0xc00035a7f8; data1 addr:0xc00035f200,data2 addr:0xc00035f208
// struct data in slice1: 824637256192 8 8
// ===
// slice2 addr 0xc00035a810; data2 addr:0xc00035f208
// struct data in slice2: 824637256200 7 7
可以看到變量slice2和slice1的地址是不一樣的。但是slice2的第一個數據的地址是,slice1第二個數據的地址, 同時底層結構體slice的array實參也相差8字節。這不是巧合,恰恰說明了兩個切片都是用的同一個底層數組,印證了上面那張圖。
我們在來看看make([]int, 1, 10)即slice make的實現源碼
// go/src/runtime/slice.go
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
func makeslice64(et *_type, len64, cap64 int64) unsafe.Pointer {
len := int(len64)
if int64(len) != len64 {
panicmakeslicelen()
}
cap := int(cap64)
if int64(cap) != cap64 {
panicmakeslicecap()
}
return makeslice(et, len, cap)
}
當切片的容量和大小大於 int類型所能表達的最大值時,就會調用makeslice64 處理;反之則調用makeslice。
可以看到最終都是調用mallocgc開闢空間。而mallocgc上具體怎麼開闢也有講究,源碼上面的備註如下:
// go/src/runtime/malloc.go
// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
}
由於怎麼開闢空間不屬於本文的範疇,有興趣的朋友可以自己查閱源碼。
操作的底層原理
我們每次調用append追加數據,都是依靠位於growslice
函數實現的。細節代碼有所省略,感興趣的讀者可以看看源碼,我們主要挑重點看
// 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
}
}
}
可以看到擴容策略如下(權重按順序依次遞減):
- 如果新申請的容量大於2倍舊容量,則新容量就是申請容量大小
- 如果舊容量長度小於1024, 則新容量就是2倍舊容量大小
- 舊容量長度大於等於1024,則新容量就是舊容量的1.25
- 如果新容量的值都溢出int的最大值,新容量就是申請容量
然後我們就來測試一下擴容策略,主要測試第二條和第三條
func TestAppend(t *testing.T) {
slice := make([]int, 0)
oldCap := cap(slice)
for i := 0; i < 4096; i++ {
slice = append(slice, i)
newCap := cap(slice)
if newCap != oldCap {
fmt.Printf("oldCap = %d after append %d newCap = %d\n", oldCap, i, newCap)
oldCap = newCap
}
}
}
執行這段測試用例後會如下輸出:
oldCap = 0 after append 0 newCap = 1
oldCap = 1 after append 1 newCap = 2
oldCap = 2 after append 2 newCap = 4
oldCap = 4 after append 4 newCap = 8
oldCap = 8 after append 8 newCap = 16
oldCap = 16 after append 16 newCap = 32
oldCap = 32 after append 32 newCap = 64
oldCap = 64 after append 64 newCap = 128
oldCap = 128 after append 128 newCap = 256
oldCap = 256 after append 256 newCap = 512
oldCap = 512 after append 512 newCap = 1024
oldCap = 1024 after append 1024 newCap = 1280
oldCap = 1280 after append 1280 newCap = 1696
oldCap = 1696 after append 1696 newCap = 2304
oldCap = 2304 after append 2304 newCap = 3072
oldCap = 3072 after append 3072 newCap = 4096
我們可以看到,newCap = 1024擴容後的newCap = 1.25 * 1025 = 1280。但1280擴容後newCap=1696 != 1280 * 1.25,這是爲什麼呢?再來看下面的代碼
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:
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) // (1280 * 1.25) * 8 = 12800
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize) // sys.PtrSize = 8
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)
}
我們主要看 et.size == sys.PtrSize
的裏面的邏輯 我們可以看到最底下的newcap依賴一個capmem的值,而capmem是roundupsize函數
的返回。我們再看看roundupsize
的源碼
// size = 12800 (可以看上一塊代碼段,12800是如何算出來的)
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize {
if size <= smallSizeMax-8 {
return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
} else {
return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
// class_to_size, smallSizeMax, size_to_class128, largeSizeDiv都是常量。 所以很容易計算
// 返回13568
}
}
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}
經過計算我們得知roundupsize函數
返回的值是13568,所以newcap = int(capmem / sys.PtrSize) = 13568 / 8
1696就是這麼得出來的。可以看出當容量大於1024後擴容,擴充因子並不是1.25,而是略大。
最後我來看一段常規的append
代碼
func TestAppend1(t *testing.T) {
slice := make([]int, 0, 2)
slice = append(slice, 1)
ptr := unsafe.Pointer(&slice)
opt := (*[3]int)(ptr)
fmt.Println("array addr: ", opt[0])
slice = append(slice, 2)
ptr = unsafe.Pointer(&slice)
opt = (*[3]int)(ptr)
fmt.Println("array addr: ", opt[0])
slice = append(slice, 3)
ptr = unsafe.Pointer(&slice)
opt = (*[3]int)(ptr)
fmt.Println("array addr: ", opt[0])
}
// 輸出:
// array addr: 824634054360
// array addr: 824634054360
// array addr: 824638172096
可以看到當slice擴容後,底層的數組地址變了。具體的源碼如下
var p unsafe.Pointer
if et.ptrdata == 0 {
// 在老的切片後面繼續擴充容量
p = mallocgc(capmem, nil, false)
// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
// Only clear the part that will not be overwritten.
// 先將 P 地址加上新的容量得到新切片容量的地址,然後將新切片容量地址後面的 capmem-newlenmem 個 bytes 這塊內存初始化。爲之後繼續 append() 操作騰出空間。
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
// 重新申請新的數組給新切片
// 重新申請 capmen 這個大的內存地址,並且初始化爲0值
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
// Only shade the pointers in old.array since we know the destination slice p
// only contains nil pointers because it has been cleared during alloc.
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
}
}
// 將 lenmem 這個多個 bytes 從 old.array地址 拷貝到 p 的地址處
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}
當 et.ptrdata == 0 的時候,這時候就直接擴底層數據的大小,而不會新建數據,然後將老數組的值遷移過去。 (但et.ptrdata == 0 具體是指的什麼類型的切片,這點博主也有點懵,如果有知道的朋友請留言告知一下,感謝)
其餘情況就是,新建數組,然後將老數據遷移過去。最後返回新的slice結構體