Golang 切片(slice)擴容機制源碼剖析


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。

例子:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM