golang 切片底層原理
1. 切片的結構
一個切片在執行時由指標、長度和容量三部分構成
指標指向切片元素對應的底層陣列元素的地址;長度對應切片中元素的數目,長度不能超過容量;容量一般是從切片的開始位置到底層陣列的結尾位置的長度
2. 切片的底層原理
在編譯時構建抽象語法樹階段會將切片構建為如下型別:
type Slice struct{ Elem *Type }
編譯時使用NewSlice函式建立一個新的切片型別,並需要傳遞切片元素的型別。從中可以看出,切片元素的型別是在編譯期間確定的
2.1 切片的make初始化
在編譯時,對於字面量的重要優化是判斷變數應該被分配在棧區還是應該逃逸到堆區
如果make函式初始化了一個太大的切片,該切片就會逃逸到堆區;如果分配了一個比較小的切片,就會被分配到棧區
這個切片大小的臨界值預設為64KB(不確定後續是否會存在優化),因此make([]int64, 1023) 和 make([]int64, 1024) 是完全不同的記憶體佈局
2.2 切片擴容原理
切片使用append函式新增元素,但不是使用了append就需要擴容
只要沒有超過當前分配的cap大小,就不會發生擴容
切片擴容的現象說明了go語言並不會在每次append時都進行擴容,也不會每增加一個元素就擴容一次,因為擴容涉及記憶體分配,將損害效能
append函式的核心在執行時呼叫了runtime/slice.go檔案下的growslice函式:
func growslice(et *_type, old slice, cap int) slice { newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { for 0 < newcap && newcap < cap { newcap += newcap / 4 }if newcap <= 0 { newcap = cap } } } ... }
上面的程式碼顯示了擴容的核心邏輯,golang中切片的擴容策略為:
- 如果申請的容量cap大於2倍舊容量old.cap,最終新的容量newcap為新申請的容量
- 如果舊的切片長度小於1024,則最終容量是舊容量的2倍
- 如果舊切片長度大於或等於1024,則最終容量從舊容量開始迴圈增加1/4,直到最終容量大於或等於新申請的容量為止
- 如果最終容量計算值溢位,即超過了int的最大範圍,則最終容量就是新申請的容量
為了記憶體對齊,申請的記憶體可能大於實際型別✖️容量大小
如果切片需要擴容,那麼最後需要在堆區申請記憶體
擴容後的新切片不一定擁有新的地址,因此在使用append函式時,通常會採用 a = append(a, T) 的方式
當切片型別不是指標,分配記憶體後只需要將記憶體後面的值清空
當切片型別為指標,設計垃圾回收寫屏障開啟時,對舊切片中的指標指向的物件進行標記
2.3 切片複製
複製的切片不會改變指向底層陣列的資料來源,但有些時候我們希望建立一個新的陣列,並且與舊陣列不共享相同的資料來源,這時可以使用copy函式:
// 建立目標切片 numbers1 := make([]int, len(numbers), cap(numbers)*2) // 將numbers元素複製到numbers1中 count := copy(numbers1, numbers)
當然切片元素也可以直接複製給一個數組,但是要考慮二者容量的問題
如果在複製時,陣列長度和切片的長度不相等,那麼複製的元素為len(arr)和len(slice)的較小值
copy函式在執行時主要呼叫了memmove函式,用於實現記憶體的複製
如果採用協程呼叫的方式go copy(arr,slice)或者加入了race檢測,則會轉而呼叫執行時slicestringcopy或者slicecopy函式,進行額外的檢查