切片擴容大法

輕描淡寫發表於2021-09-09

Q:你先簡單介紹一下自己吧
A:嘛哩嘛哩哄,嘛哩嘛哩哄,嘛哩嘛哩哄
Q:你說這麼多嘛哩嘛哩哄,那你瞭解陣列嗎?
A:陣列是具有相同型別且長度固定的資料結構,由於長度不可改變,在實際中使用較少,一般用切片來代替使用。
Q:你剛剛說到切片,切片和陣列有什麼相同點和不同點嗎?
A:相同點是他們都是隻能儲存相同型別的資料結構,切片的底層資料也是基於陣列的,不同點是切片是可以改變長度的,能自動擴容。
Q:嗯,那切片的底層是怎麼組成的呢,它的擴容規則你清楚嗎?
A:切片由三部分組成,分別是切片的長度,切片的容量,切片的資料(指向底層陣列的指標),至於擴容規則可就難說了。
Q:難說也是要說的,你搞快點。
A:切片擴容的基本規則是:當切片append的時候(會呼叫下面的函式),新切片的必須容量大於老切片容量的兩倍的,那麼新切片的容量就是新切片的必須容量,反之,如果老切片的長度小於1024,那麼新切片的容量就等於老切片的容量的兩倍,如果老切片大於等於1024,則新切片容量等於老切片容量的1.25倍,這是切片的預估容量。

// runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
//et 表示slice的一個元素,old 表示舊的slice cap 表示新切片必須的容量
    ...
    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
            }
        }
    }
    ...
}

根據以上規則 ,一下切片擴容必須的切片容量為 5 ,5 > 2*2,所以列印出來應該是len=5, cap=5

package main

import (
    "fmt"
)

func main() {
    s := []int{1, 2}
    s = append(s, 4, 5, 6)
    fmt.Printf("len=%d, cap=%d", len(s), cap(s))
}

但是實際列印卻是 len=5, cap=6,原來容量計算完了後還要考慮到記憶體的高效利用,進行記憶體對齊,則會呼叫這個函式

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 alignUp(size, _PageSize)
}

size 表示新切片需要的記憶體大小 我們傳入的int 型別,每個佔用8位元組(可以呼叫unsafe.Sizeof()函式檢視佔用的大小),一共5個 所以是40,size小於_MaxSmallSize 並且小於 smallSizeMax-8 ,那麼使用通用公式(size+smallSizeDiv-1)/smallSizeDiv 計算得到 5,
然後到size_to_class8 找到第五號元素 為 4,再從 class_to_size 找到 第四號元素 為 48,這就是新切片佔用的記憶體大小,每個int 佔用 8 位元組,所以最終切片的容量為 6 。所以說切片的擴容有它基本的擴容規則,在規則之後還要考慮記憶體對齊,這就代表不同資料型別的切片擴容的容量大小是會不一致。
Q:好,那我們進入下一個問題

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章