Golang 深入 slice 實現原理及使用技巧
slice 使用總結,持續更新於我的Github
介紹
我們都知道array是固定長度的陣列, slice是對array的擴充套件,本質上是基於陣列實現的,主要特點是定義完一個slice變數之後,不需要為它的容量而擔心。 本文記錄直接深入slice的底層實現原理,不再介紹slice的基本使用。
slice 結構
- slice中 array 是一個指標,它指向的是一個array
- len 代表的是這個slice中的元素長度
- cap 是slice的容量
-
參考 Golang slice 原始碼
type slice struct { array unsafe.Pointer lenint capint } 複製程式碼
slice 擴容
s := []int{1,2,3,4,5,6} s = append(s, 6) 複製程式碼
- 如果新的slice大小是當前大小2倍以上,則大小增長為新大小
- 如果當前slice cap 小於1024,按每次2倍增長,否則每次按當前大小1/4增長。直到增長的大小超過或等於新大小
- append的實現是在記憶體中將slice的array值賦值到新申請的array上
- 擴容原始碼實現
效能
- 通過上面我們知道slice的擴容涉及到記憶體的拷貝,這樣帶來的好處是資料儲存在連續記憶體上,比隨機訪問快很多,最直接的效能提升就是快取命中率會高很多,這也就是為什麼slice不採用動態連結串列實現的原因吧
- 我們知道拷貝記憶體資料是有開銷的, 而其中最大的開銷不在 memmove 資料上,而是在開闢一塊新記憶體malloc及之後的GC壓力
- 拷貝連續記憶體是很快的,隨著cap變大,拷貝總成本還是 O(N) ,只是常數大了
- 假如不想發生拷貝,那你就沒有連續記憶體。此時隨機訪問開銷會是:連結串列 O(N), 2倍增長塊鏈 O(LogN), 二級表一個常數很大的 O(1)
- 當你能大致知道所需的最大空間(在大部分時候都是的)時,在make的時候預留相應的 cap 就好
- 如果需要的空間很大,而且每次都不確定,那就要在浪費記憶體和耗 CPU 在 malloc + gc 上做權衡
- 連結串列的查詢操作是從第一個元素開始,所以相對陣列要耗時間的多,因為採用這樣的結構對讀的效能有很大的提高
選擇
- slice是很靈活的,大部分情況都能表現的很好
- 但也有特殊情況,slice的容量超大並且需要頻繁的更改slice的內容時,改用list更合適