Go切片擴(kuò)容機(jī)制詳細(xì)說明和舉例
本文對(duì)于切片擴(kuò)容做了非常詳細(xì)的說明和舉例,篇幅較長,行文不易,一字一句純手打創(chuàng)造,傾注了不少精力,感謝支持。
切片擴(kuò)容的理解
關(guān)于切片的“擴(kuò)容”,我們先來理解一下有一個(gè)初印象。
擴(kuò)容實(shí)際上就是因?yàn)?strong>已有容量不足以容納新元素(經(jīng)過追加后的長度>已有容量時(shí)),因此需要結(jié)合原切片本身及所需要的新容量來分配一塊新的內(nèi)存空間,經(jīng)過擴(kuò)容操作,新的容量就確定了。
同時(shí)因?yàn)榍衅牡讓右彩峭ㄟ^數(shù)組來存儲(chǔ)數(shù)據(jù),每次擴(kuò)容時(shí)舊數(shù)組必定已滿,而數(shù)組固定定長是無法擴(kuò)容的,因此切片擴(kuò)容后也會(huì)使用一個(gè)新數(shù)組,而非舊數(shù)組,舊數(shù)組中的數(shù)據(jù)當(dāng)然會(huì)全部復(fù)制到新數(shù)組中。
值得注意的是,每次擴(kuò)容時(shí)的重點(diǎn)是“cap”,而非底層數(shù)組的“長度”。
擴(kuò)容機(jī)制源碼分析
因?yàn)槠渲械倪壿嫴呗陨詮?fù)雜,先貼出所有源碼然后分別分析,源碼位于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函數(shù)用來處理追加過程中的切片增長
2,該函數(shù)傳遞切片元素類型 et、舊切片old、期望的新最小容量cap,其中舊切片old是一個(gè)slice類型的對(duì)象,其結(jié)構(gòu)是:
type slice struct { array unsafe.Pointer // 指針,指向可訪問到的第一個(gè)元素 len int // 長度 cap int // 容量 }
返回一個(gè)至少具有該容量的新切片,新切片中會(huì)包含舊數(shù)據(jù)。
其中首要的核心邏輯:
newcap := old.cap // old.cap是舊容量 doublecap := newcap + newcap // 計(jì)算出舊容量的兩倍 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)處理,直到達(dá)到期望容量 newcap += newcap / 4 // 一次增加25% } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } }
細(xì)節(jié):0 < newcap 條件是用來判斷是否溢出,如果溢出超過整型最大值,則也會(huì)終止。
總結(jié):
1,期望的容量(cap)如果大于舊容量的兩倍,則新容量直接設(shè)置為期望容量,反之第二條;
2,舊容量(old.cap)和1024進(jìn)行比較,如果小于1024,則新容量設(shè)置為兩倍的舊容量大小,反之第三條;
3,如果大于1024則循環(huán)處理,每次以25%的增長速度(1.25倍)增長,newcap不斷增加直到達(dá)到期望容量的大小。
以1024判斷也會(huì)出現(xiàn)一些不友好的現(xiàn)象,但使用起來正常,這個(gè)后面再詳細(xì)說。
我們按這個(gè)規(guī)則驗(yàn)證, 按自然數(shù)依次增加不斷追加簡單計(jì)算一下:
arr1 := []int{} fmt.Printf("(1) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1) // 追加1個(gè)值 arr1 = append(arr1, 1) // 相當(dāng)于索引為0的位置的數(shù)據(jù)被設(shè)置為1 fmt.Printf("(2) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1) // 追加2個(gè)值 arr1 = append(arr1, 2, 3) fmt.Printf("(3) len=%v, cap=%v, addr:%p, arr1: %v\n", len(arr1), cap(arr1), &arr1, arr1) // 追加3個(gè)值 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個(gè)值 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個(gè)值 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個(gè)值 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個(gè)值 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個(gè)值 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這個(gè)數(shù)據(jù)時(shí),期望新容量=1,1>舊容量0的兩倍,cap變?yōu)?;
增加2、3時(shí),期望新容量=3,舊容量1的兩倍=2,3>2 ,cap變?yōu)?;
增加4、5、6時(shí),期望新容量=6,舊容量3的兩倍=6, 與1024比較<1024,cap變?yōu)?;
增加7、8、9、10時(shí),期望新容量=10,舊容量6的兩倍=12, 與1024比較<1024,cap變?yōu)?2;
...
看起來還正常,OK,我們?cè)倏匆粋€(gè)直接、快捷一點(diǎn)的例子:
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,追加了一個(gè)數(shù)容量就變?yōu)榱?44?,推一下:
追加1時(shí),期望新容量=67,舊容量66的兩倍=132,67<132 ,與1024比較<1024,cap變?yōu)?32;
不應(yīng)該是132嗎,為什么結(jié)果是144 ???
繼續(xù)看下一節(jié)。
分配大小修正/cap調(diào)整
上一小節(jié)所總結(jié)的邏輯/規(guī)則并不是不對(duì),它是正確的,但在計(jì)算得出newcap后又進(jìn)行了另一段邏輯,然后得出了一個(gè)新的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) }
這個(gè)switch分支根據(jù)切片元素類型(et)所占的大?。硞€(gè)類型如int所占的空間)來判斷,其中核心的函數(shù)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 }
用以根據(jù)申請(qǐng)的空間大小返回實(shí)際分配的內(nèi)存大小。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。它也是機(jī)器本機(jī)單詞大小的大?。?,32位系統(tǒng)上為4,64位系統(tǒng)中為8)。
因此基于本機(jī)PtrSize 的值=8。
case條件中元素類型的大小又是怎么回事? 寫個(gè)例子你就明白了:
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位的機(jī)器上,et.size的值為8,基于此,現(xiàn)在走一下對(duì)應(yīng)的case,按上述66追加1那個(gè)例子推演:
switch { case et.size == 1: // 空間大小為1時(shí) // ... 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)計(jì)算: 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 因此經(jīng)過計(jì)算,newcap=144, 并不是132
破案了?。。?nbsp;
后面的修正邏輯的意義在于,僅以容量和數(shù)字判斷進(jìn)行單純的計(jì)算來得到新容量較片面,忽略了切片的類型對(duì)應(yīng)所占的空間大小等因素的影響,經(jīng)過這一步內(nèi)存分配的二次計(jì)算,才會(huì)返回一個(gè)實(shí)際要分配的容量大小作為newcap。
那么這下我們掌握了正確、完整的計(jì)算邏輯,假設(shè)隨便找一個(gè)容量的切片進(jìn)行追加:
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)計(jì)算: 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。
這次先到這里,再會(huì)!
總結(jié)
到此這篇關(guān)于Go切片擴(kuò)容機(jī)制詳細(xì)說明和舉例的文章就介紹到這了,更多相關(guān)Go切片擴(kuò)容機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Go語言如何實(shí)現(xiàn)并發(fā)安全的map
go語言提供的數(shù)據(jù)類型中,只有channel是并發(fā)安全的,基礎(chǔ)map并不是并發(fā)安全的,本文為大家整理了三種實(shí)現(xiàn)了并發(fā)安全的map的方案,有需要的可以參考下2023-12-12Go Uber靜態(tài)分析工具NilAway使用初體驗(yàn)
這篇文章主要介紹了Go Uber靜態(tài)分析工具NilAway使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01go語言中io操作中的 io.Reader 和 io.Writer的獲取方法
在Go語言中,要進(jìn)行文件io操作,通常需要使用io.Reader或io.Writer對(duì)象,獲取這些對(duì)象的方法包括使用標(biāo)準(zhǔn)庫中已實(shí)現(xiàn)Read或Write方法的對(duì)象,感興趣的可以了解一下2024-10-10GO excelize讀取excel進(jìn)行時(shí)間類型轉(zhuǎn)換的示例代碼(自動(dòng)轉(zhuǎn)換)
我們經(jīng)常會(huì)遇到如何自動(dòng)識(shí)別excel中的時(shí)間類型數(shù)據(jù)并轉(zhuǎn)化成對(duì)應(yīng)的 "Y-m-d H:i:s"類型數(shù)據(jù),本文小編給大家介紹了GO excelize讀取excel進(jìn)行時(shí)間類型轉(zhuǎn)換的示例代碼(自動(dòng)轉(zhuǎn)換),需要的朋友可以參考下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-02go語言實(shí)現(xiàn)http服務(wù)端與客戶端的例子
今天小編就為大家分享一篇go語言實(shí)現(xiàn)http服務(wù)端與客戶端的例子,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-08-08