Go切片擴容機制詳細說明和舉例
本文對于切片擴容做了非常詳細的說明和舉例,篇幅較長,行文不易,一字一句純手打創(chuàng)造,傾注了不少精力,感謝支持。
切片擴容的理解
關于切片的“擴容”,我們先來理解一下有一個初印象。
擴容實際上就是因為已有容量不足以容納新元素(經過追加后的長度>已有容量時),因此需要結合原切片本身及所需要的新容量來分配一塊新的內存空間,經過擴容操作,新的容量就確定了。
同時因為切片的底層也是通過數組來存儲數據,每次擴容時舊數組必定已滿,而數組固定定長是無法擴容的,因此切片擴容后也會使用一個新數組,而非舊數組,舊數組中的數據當然會全部復制到新數組中。
值得注意的是,每次擴容時的重點是“cap”,而非底層數組的“長度”。
擴容機制源碼分析
因為其中的邏輯策略稍復雜,先貼出所有源碼然后分別分析,源碼位于src/runtime/slice.go
(本文中涉及引用的源碼版本為go1.17.13)
// 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.
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
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}
}
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
}
}
}
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)
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)
}
// The check of overflow in addition to capmem > maxAlloc is needed
// to prevent an overflow which can be used to trigger a segfault
// on 32bit architectures with this example program:
//
// type T [1<<27 + 1]int64
//
// var d T
// var s []T
//
// func main() {
// s = append(s, d, d, d, d)
// print(len(s), "\n")
// }
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
}
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.
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
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)
}
}
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}首先從注釋可以得到:
1,growtslice函數用來處理追加過程中的切片增長
2,該函數傳遞切片元素類型 et、舊切片old、期望的新最小容量cap,其中舊切片old是一個slice類型的對象,其結構是:
type slice struct {
array unsafe.Pointer // 指針,指向可訪問到的第一個元素
len int // 長度
cap int // 容量
}返回一個至少具有該容量的新切片,新切片中會包含舊數據。
其中首要的核心邏輯:
newcap := old.cap // old.cap是舊容量
doublecap := newcap + newcap // 計算出舊容量的兩倍
if cap > doublecap { // 期望的容量大于舊容量的兩倍
newcap = cap // 新容量=期望的容量
} else {
if old.cap < 1024 { // 和1024比較
newcap = doublecap // 如果小于1024,新容量=舊容量的兩倍
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap { // 循環(huán)處理,直到達到期望容量
newcap += newcap / 4 // 一次增加25%
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}細節(jié):0 < newcap 條件是用來判斷是否溢出,如果溢出超過整型最大值,則也會終止。
總結:
1,期望的容量(cap)如果大于舊容量的兩倍,則新容量直接設置為期望容量,反之第二條;
2,舊容量(old.cap)和1024進行比較,如果小于1024,則新容量設置為兩倍的舊容量大小,反之第三條;
3,如果大于1024則循環(huán)處理,每次以25%的增長速度(1.25倍)增長,newcap不斷增加直到達到期望容量的大小。
以1024判斷也會出現一些不友好的現象,但使用起來正常,這個后面再詳細說。
我們按這個規(guī)則驗證, 按自然數依次增加不斷追加簡單計算一下:
arr1 := []int{}
fmt.Printf("(1) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1)
// 追加1個值
arr1 = append(arr1, 1) // 相當于索引為0的位置的數據被設置為1
fmt.Printf("(2) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1)
// 追加2個值
arr1 = append(arr1, 2, 3)
fmt.Printf("(3) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1)
// 追加3個值
arr1 = append(arr1, 4, 5, 6)
fmt.Printf("(4) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1)
// 追加4個值
arr1 = append(arr1, 7, 8, 9, 10)
fmt.Printf("(5) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1)
// 追加5個值
arr1 = append(arr1, 11, 12, 13, 14, 15)
fmt.Printf("(6) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1)
// 追加6個值
arr1 = append(arr1, 16, 17, 18, 19, 20, 21)
fmt.Printf("(7) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1)
// 追加7個值
arr1 = append(arr1, 22, 23, 24, 25, 26, 27, 28)
fmt.Printf("(8) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1)
// 追加8個值
arr1 = append(arr1, 29, 30, 31, 32, 33, 34, 35, 36)
fmt.Printf("(9) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1)這次是每次增加的元素依次遞增,看看有什么不一樣:
(1) len=0, cap=0, addr:0xc000004078, arr1: [] (2) len=1, cap=1, addr:0xc000004078, arr1: [1] (3) len=3, cap=3, addr:0xc000004078, arr1: [1 2 3] (4) len=6, cap=6, addr:0xc000004078, arr1: [1 2 3 4 5 6] (5) len=10, cap=12, addr:0xc000004078, arr1: [1 2 3 4 5 6 7 8 9 10] (6) len=15, cap=24, addr:0xc000004078, arr1: [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] (7) len=21, cap=24, addr:0xc000004078, arr1: [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21] (8) len=28, cap=48, addr:0xc000004078, arr1: [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28] (9) len=36, cap=48, addr:0xc000004078, arr1: [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36]
增加1這個數據時,期望新容量=1,1>舊容量0的兩倍,cap變?yōu)?;
增加2、3時,期望新容量=3,舊容量1的兩倍=2,3>2 ,cap變?yōu)?;
增加4、5、6時,期望新容量=6,舊容量3的兩倍=6, 與1024比較<1024,cap變?yōu)?;
增加7、8、9、10時,期望新容量=10,舊容量6的兩倍=12, 與1024比較<1024,cap變?yōu)?2;
...
看起來還正常,OK,我們再看一個直接、快捷一點的例子:
arr := make([]int, 66)
fmt.Printf("(1) len=%v, cap=%v, arr: %v\n", len(arr), cap(arr), arr)
arr = append(arr, 1)
fmt.Printf("(1) len=%v, cap=%v, arr: %v\n", len(arr), cap(arr), arr)speed running:
(1) len=66, cap=66, arr: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] (2) len=67, cap=144, arr: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ]
新創(chuàng)建的切片容量是66,追加了一個數容量就變?yōu)榱?44?,推一下:
追加1時,期望新容量=67,舊容量66的兩倍=132,67<132 ,與1024比較<1024,cap變?yōu)?32;
不應該是132嗎,為什么結果是144 ???
繼續(xù)看下一節(jié)。
分配大小修正/cap調整
上一小節(jié)所總結的邏輯/規(guī)則并不是不對,它是正確的,但在計算得出newcap后又進行了另一段邏輯,然后得出了一個新的newcap,因此與上一步所得的newcap不相等是有可能的,一起看看下一段邏輯:
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)
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)
}這個switch分支根據切片元素類型(et)所占的大小(某個類型如int所占的空間)來判斷,其中核心的函數roundupsize:
// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize { // 小于32768(32K)
if size <= smallSizeMax-8 { // 如果 size <= 1024-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)]])
}
}
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}
// divRoundUp returns ceil(n / a).
func divRoundUp(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
}
用以根據申請的空間大小返回實際分配的內存大小。sys.PtrSize是什么?
// PtrSize is the size of a pointer in bytes - unsafe.Sizeof(uintptr(0)) but as an ideal constant. // It is also the size of the machine's native word size (that is, 4 on 32-bit systems, 8 on 64-bit). const PtrSize = 4 << (^uintptr(0) >> 63)
PtrSize是指針的大小,以字節(jié)為單位,大小是unsafe.Sizeof(uintptr(0)),即=8。它也是機器本機單詞大小的大?。?,32位系統(tǒng)上為4,64位系統(tǒng)中為8)。
因此基于本機PtrSize 的值=8。
case條件中元素類型的大小又是怎么回事? 寫個例子你就明白了:
var a int
fmt.Println("int size = ", unsafe.Sizeof(a)) // 8
var b bool
fmt.Println("bool size = ", unsafe.Sizeof(b)) // 1
var c uint
fmt.Println("uint size = ", unsafe.Sizeof(c)) // 8
fmt.Println("string size = ", unsafe.Sizeof("")) // 16
var d int8 = 127
fmt.Println("int8 size = ", unsafe.Sizeof(d)) // 1
var e int16
fmt.Println("int16 size = ", unsafe.Sizeof(e)) // 2
var f int64
fmt.Println("int64 size = ", unsafe.Sizeof(f)) // 8也就是說如果是int類型的切片,在64位的機器上,et.size的值為8,基于此,現在走一下對應的case,按上述66追加1那個例子推演:
switch {
case et.size == 1: // 空間大小為1時
// ...
case et.size == sys.PtrSize: // 8
lenmem = uintptr(old.len) * sys.PtrSize // 66*8=528
newlenmem = uintptr(cap) * sys.PtrSize // 67 * 8 =536
capmem = roundupsize(uintptr(newcap) * sys.PtrSize) // roundupsize(132 * 8) = roundupsize(1056)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
// ...
default:
// ...
按roundupsize(1056)計算:
divRoundUp(size-smallSizeMax, largeSizeDiv) = divRoundUp(32,128) = 1
size_to_class128[1] = 33
class_to_size[33] = 1152
newcap = int(capmem / sys.PtrSize) = 1152 / 8 = 144
因此經過計算,newcap=144, 并不是132破案了?。?!
后面的修正邏輯的意義在于,僅以容量和數字判斷進行單純的計算來得到新容量較片面,忽略了切片的類型對應所占的空間大小等因素的影響,經過這一步內存分配的二次計算,才會返回一個實際要分配的容量大小作為newcap。
那么這下我們掌握了正確、完整的計算邏輯,假設隨便找一個容量的切片進行追加:
arr := make([]int, 88)
fmt.Printf("(1) len=%v, cap=%v\n", len(arr), cap(arr))
arr = append(arr, 1)
fmt.Printf("(2) len=%v, cap=%v\n", len(arr), cap(arr))我們自己推算一下:
capmem = roundupsize(uintptr(newcap) * sys.PtrSize) = roundupsize(1408) 按roundupsize(1408)計算: divRoundUp(384,128) = 3 size_to_class128[3] = 35 class_to_size[35] = 1408 newcap = 1408 / 8 = 176
來看看是不是176,speed running:
(1) len=88, cap=88 (2) len=89, cap=176
very good。
這次先到這里,再會!
總結
到此這篇關于Go切片擴容機制詳細說明和舉例的文章就介紹到這了,更多相關Go切片擴容機制內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Go Uber靜態(tài)分析工具NilAway使用初體驗
這篇文章主要介紹了Go Uber靜態(tài)分析工具NilAway使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01
go語言中io操作中的 io.Reader 和 io.Writer的獲取方法
在Go語言中,要進行文件io操作,通常需要使用io.Reader或io.Writer對象,獲取這些對象的方法包括使用標準庫中已實現Read或Write方法的對象,感興趣的可以了解一下2024-10-10
GO excelize讀取excel進行時間類型轉換的示例代碼(自動轉換)
我們經常會遇到如何自動識別excel中的時間類型數據并轉化成對應的 "Y-m-d H:i:s"類型數據,本文小編給大家介紹了GO excelize讀取excel進行時間類型轉換的示例代碼(自動轉換),需要的朋友可以參考下2024-10-10
解決golang編譯提示dial tcp 172.217.160.113:443: con
這篇文章主要介紹了解決golang編譯提示dial tcp 172.217.160.113:443: connectex: A connection attempt failed,此問題完美解決,需要的朋友可以參考下2023-02-02

