https://blog.csdn.net/nyist_zxp/article/details/111425091
https://blog.csdn.net/weixin_37509194/article/details/112001014
https://blog.csdn.net/qq_43971008/article/details/105385434
一、源碼
Version : go1.15.6 src/runtime/slice.go
//go1.15.6 源碼 src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { //省略部分判斷代碼 //計算擴容部分 //其中,cap : 所需容量,newcap : 最終申請容量 newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 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 } } } //省略部分判斷代碼 }
二、原理
1. 如果當前所需容量 (cap) 大於原先容量的兩倍 (doublecap),則最終申請容量(newcap)為當前所需容量(cap);
2. 如果<條件1>不滿足,表示當前所需容量(cap)不大於原容量的兩倍(doublecap),則進行如下判斷;
3. 如果原切片長度(old.len)小於1024,則最終申請容量(newcap)等於原容量的兩倍(doublecap);
4. 否則,最終申請容量(newcap,初始值等於 old.cap)每次增加 newcap/4,直到大於所需容量(cap)為止,然后,判斷最終申請容量(newcap)是否溢出,如果溢出,最終申請容量(newcap)等於所需容量(cap);
數組
切片是對數組的封裝,所以討論切片前必須對數組有足夠的了解。
先來看下面一段代碼:
demo1 := [2]uint8{255, 255} demo2 := [2]uint16{65535, 65535} demo3 := [2]uint32{4294967295, 4294967295} demo4 := [2]uint64{18446744073709551615, 18446744073709551615} fmt.Println("[2]uint8") fmt.Println(reflect.ValueOf(&demo1[0]).Pointer()) fmt.Println(reflect.ValueOf(&demo1[1]).Pointer()) fmt.Println("[2]uint16") fmt.Println(reflect.ValueOf(&demo2[0]).Pointer()) fmt.Println(reflect.ValueOf(&demo2[1]).Pointer()) fmt.Println("[2]uint32") fmt.Println(reflect.ValueOf(&demo3[0]).Pointer()) fmt.Println(reflect.ValueOf(&demo3[1]).Pointer()) fmt.Println("[2]uint64") fmt.Println(reflect.ValueOf(&demo4[0]).Pointer()) fmt.Println(reflect.ValueOf(&demo4[1]).Pointer())
運行結果為:
[2]uint8 824633802944 824633802945 [2]uint16 824633802948 824633802950 [2]uint32 824633802952 824633802956 [2]uint64 824633802960 824633802968
一個uint8占一個字節,所以第二個元素的指針值比第一個大1,一個uint16占兩個字節,第二個元素指針值比第一個大2,以此類推。所以golang的數組在內存里面是一片連續的空間,這樣在取值的時候,指針只需要依次移動8的倍數位(一字節等於8位)就能得到相應的值。
數組變量代表的是整個數組而不是指向首元素地址的指針,所以這玩意兒在變量間賦值的時候是值傳遞而不是引用傳遞,來看看下面的代碼:
demo1 := [1]uint{1} demo2 := demo1 fmt.Println(reflect.ValueOf(&demo1[0]).Pointer()) fmt.Println(reflect.ValueOf(&demo2[0]).Pointer())
運行結果:
824633802944 824633802952
於是,程序運行的時候你電腦的內存里面存了兩個值完全相同的數組。
數組變量代表的整個數組,但變量存儲於棧區數組存儲於堆區,因此這個變量總得指向啥東西吧:
demo := [2]uint{1,2} fmt.Println(reflect.ValueOf(&demo).Pointer()) fmt.Println(reflect.ValueOf(&demo[0]).Pointer()) fmt.Println(reflect.ValueOf(&demo[1]).Pointer())
運行結果
824633802944 824633802944 824633802952
OK,懂c語言的朋友就知道了,這玩意兒和c的數組就是一樣的機制,棧區的變量指向堆區數組中的第一個元素,但千萬要注意與c不同的是賦值的時候是值傳遞而不是引用傳遞。
切片
接下來說說切片,不知道這玩意兒為什么叫切片不叫切塊。
相信看過文檔的朋友都對這張圖非常熟悉:
那個ptr就是指針,指向內存中一片連續的空間,也就是前文說的數組。切片使用了結構體來對數組進行封裝,那個結構體長這德行:
也許有些朋友自己想看一眼源碼——/$goroot$/src/runtime/slice.go第13行。
初始化一個切片的時候,操作系統會分配相應大小的連續空間給它。當元素填滿了切片后,必須進行空間擴容,但這並不是簡單地在后面添加就完事了,必須重新申請一塊更大的空間,把數據從舊空間復制到新空間,然后銷毀舊空間。看看下面代碼:
demo := []int{1} fmt.Println("擴容前cap:",cap(demo),"一號元素地址:", reflect.ValueOf(&demo[0]).Pointer()) demo = append(demo, 1) fmt.Println("擴容后cap:",cap(demo),"一號元素地址:", reflect.ValueOf(&demo[0]).Pointer())
運行結果:
擴容前cap: 1 一號元素地址: 824634163304 擴容后cap: 2 一號元素地址: 824634163376
網上相當多的文章說切片的擴容機制是小於1024的時候擴為原來的兩倍,大於時擴為原來的1.25倍,看起來說得頭頭是道,好的那咱就來試試:
demo := make([]int, 100) fmt.Println("擴容前容量:",cap(demo)) demo = append(demo, 1) fmt.Println("擴容后容量:",cap(demo))
運行結果:
100 * 2 = 224!!!!
1024以下的不好使我們來試試1024以上的,這次玩個大的來個10000容量:
demo := make([]int, 10000) fmt.Println("擴容前容量:",cap(demo)) demo = append(demo, 1) fmt.Println("擴容后容量:",cap(demo))
運行結果
10000 * 1.25 = 13312。。。???
看來其他文章里面說的不大好使。
那當然要不好使了,要不然這片文章怎么能說另有玄機。
現在咱去研究切片的源碼。切片的擴容函數也在slice.go里面,第76行的growslice()就是了。小於1024擴為兩倍大於擴為1.25倍這個算法也不能說錯,因為函數中有這么一段代碼:
newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 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 } } }
但是除了這段外還有下面這一段:
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分支case的東西我搞了好大半天才明白,它是類型大小,即一個類型所占的空間。可別想着在這里用fmt.Pringln()打印出這個類型大小,一導入fmt這個包,就會得到這幾個字:import cycle not allowed。
別問我怎么知道的,我是不可能告訴你們的。
所以,要得到這個類型大小……偉大的反射登場吧!
demo1 := true fmt.Println("bool: ", reflect.TypeOf(demo1).Size()) demo2 := 'a' fmt.Println("rune: ", reflect.TypeOf(demo2).Size()) demo3 := 1 fmt.Println("int: ", reflect.TypeOf(demo3).Size()) demo4 := "a" fmt.Println("string: ", reflect.TypeOf(demo4).Size())
Let us see see 運行結果。
bool: 1 rune: 4 int: 8 string: 16
接下來就是roundupsize()函數,知道它是啥意思么,不知道沒事我也不知道,咱點開看看唄。
好的這是msize.go里面的函數。這個go文件當頭一句注釋就是“Malloc small size classes.”
OK,現在成功扯上了內存管理。
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 { 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]]) } } if size+_PageSize < size { return size } return round(size, _PageSize) }
一切的玄機就在這個函數里面,它的作用根據申請空間大小返回實際分配空間大小。如果申請空間小於_MaxSmallSize也就是32678也就是32K的時候,它利用smallSizeDiv、smallSizeMax、largeSizeDiv計算索引來從size_to_class和class_to_size中查值返回。
讓什么smallSizeDiv、smallSizeMax、largeSizeDiv、size_to_class和class_to_size見鬼去吧,這么說誰聽得懂啊,這些什么鬼玩意兒啊!
下面咱看看這張表。
// class bytes/obj bytes/span objects tail waste max waste // 1 8 8192 1024 0 87.50% // 2 16 8192 512 0 43.75% // 3 32 8192 256 0 46.88% // 4 48 8192 170 32 31.52% // 5 64 8192 128 0 23.44% // 6 80 8192 102 32 19.07% // 7 96 8192 85 32 15.95% // 8 112 8192 73 16 13.56% // 9 128 8192 64 0 11.72% // 10 144 8192 56 128 11.82% // 11 160 8192 51 32 9.73% // 12 176 8192 46 96 9.59% // 13 192 8192 42 128 9.25% // 14 208 8192 39 80 8.12% // 15 224 8192 36 128 8.15% // 16 240 8192 34 32 6.62% // 17 256 8192 32 0 5.86% // 18 288 8192 28 128 12.16% // 19 320 8192 25 192 11.80% // 20 352 8192 23 96 9.88% // 21 384 8192 21 128 9.51% // 22 416 8192 19 288 10.71% // 23 448 8192 18 128 8.37% // 24 480 8192 17 32 6.82% // 25 512 8192 16 0 6.05% // 26 576 8192 14 128 12.33% // 27 640 8192 12 512 15.48% // 28 704 8192 11 448 13.93% // 29 768 8192 10 512 13.94% // 30 896 8192 9 128 15.52% // 31 1024 8192 8 0 12.40% // 32 1152 8192 7 128 12.41% // 33 1280 8192 6 512 15.55% // 34 1408 16384 11 896 14.00% // 35 1536 8192 5 512 14.00% // 36 1792 16384 9 256 15.57% // 37 2048 8192 4 0 12.45% // 38 2304 16384 7 256 12.46% // 39 2688 8192 3 128 15.59% // 40 3072 24576 8 0 12.47% // 41 3200 16384 5 384 6.22% // 42 3456 24576 7 384 8.83% // 43 4096 8192 2 0 15.60% // 44 4864 24576 5 256 16.65% // 45 5376 16384 3 256 10.92% // 46 6144 24576 4 0 12.48% // 47 6528 32768 5 128 6.23% // 48 6784 40960 6 256 4.36% // 49 6912 49152 7 768 3.37% // 50 8192 8192 1 0 15.61% // 51 9472 57344 6 512 14.28% // 52 9728 49152 5 512 3.64% // 53 10240 40960 4 0 4.99% // 54 10880 32768 3 128 6.24% // 55 12288 24576 2 0 11.45% // 56 13568 40960 3 256 9.99% // 57 14336 57344 4 0 5.35% // 58 16384 16384 1 0 12.49% // 59 18432 73728 4 0 11.11% // 60 19072 57344 3 128 3.57% // 61 20480 40960 2 0 6.87% // 62 21760 65536 3 256 6.25% // 63 24576 24576 1 0 11.45% // 64 27264 81920 3 128 10.00% // 65 28672 57344 2 0 4.91% // 66 32768 32768 1 0 12.50%
那個啥,請只關注bytes/obj列,其它列我懶得刪了,咳咳。
這一張表在/$goroot$/src/runtime/sizeclass.go里面。
現在咱把bytes/obj列的每兩個數據看成(8~16],(16~32]這種形式,當某個數據落入這個區間時返回區間最大值。講人話就是你給他9或10它給你16,給他17或18它給你32。以此類推。
smallSizeDiv、smallSizeMax、largeSizeDiv、size_to_class*和class_to_size的作用就是上一個段落。
如果申請空間大於32K,那就會使用/$goroot$/src/runtime/stubs.go里面round()函數來計算實際返回空間。
// round n up to a multiple of a. a must be a power of 2. func round(n, a uintptr) uintptr { return (n + a - 1) &^ (a - 1) }
這個函數的作用是將n轉化為a的倍數,a必須為2的冪數。
位運算牛逼!不接受任何辯駁!
好了,現在咱重新來看看這個例子:
demo := make([]int, 100) fmt.Println("擴容前容量:",cap(demo)) demo = append(demo, 1) fmt.Println("擴容后容量:",cap(demo))
int的類型大小是8,因此在switch分支中執行了這一段代碼:
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)
sys.PtrSize,根據名稱來看就是pointer size,指針大小,在我的機器上指針大小是8。
這里面只需要關注兩個參數:capmem和newcap。開始執行的時候,經過了小於1024擴為兩倍的計算,newcap的值現在為200,因此傳入roundupsize()函數的值為1600,根據那張表,得到函數返回值為1796。
算算1796除以8后向下取整的值吧,是不是224?嘿嘿!
至於為什么要向下取整,相信朋友們不會忘了浮點型轉化為整型是怎么玩的。
所以,切片的擴容並不只是將其轉化為2倍或者1.25倍就完事了,接下來還要根據切片元素的類型大小來二次擴容。這一部分已經涉及到golang的內存分配,如果想要進一步深入,還需要到slice.go和msize.go文件中去看源碼,它們都在/$goroot$/src/runtime文件夾下(好吧我承認這里不寫出來的原因是懶得寫了hhhhh,但看一遍源碼總比單看我這文章好),當然配合我這片文章來看感覺會更絲滑,因為我已經把那個類型大小和內存分配原理給解決了。
PS:補充下當類型大小是2的冪數時會執行這一段代碼:
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)
其中shift的值是log2(et.size) & 63或者log2(et.size) & 31。
例子: