Dig101: Go 之靈活的 slice
檢視更多:歷史集錄
Dig101: dig more, simplified more and know more
Slice 作為 go 常用的資料型別,在日常編碼中非常常見。 相對於陣列的定長不可變,slice 使用起來就靈活了許多。
0x01 slice 到底是什麼?
首先我們看下原始碼中 slice 結構的定義
// src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice 資料結構如上,Data 指向底層引用的陣列記憶體地址, len 是已用長度,cap 是總容量。 為驗證如上所述,我們嘗試宣告一個 slice a,獲取 a 的 sliceHeader 頭資訊,並用%p
獲取&a, sh, a, a[0]
的地址
看看他們的地址是否相同。
a := make([]int, 1, 3)
//reflect.SliceHeader 為 slice執行時資料結構
sh := (*reflect.SliceHeader)(unsafe.Pointer(&a))
fmt.Printf("slice header: %#v\naddress of a: %p &a[0]: %p | &a: %p sh:%p ",
sh, a, &a[0],&a, sh)
//slice header: &reflect.SliceHeader{Data:0xc000018260, Len:1, Cap:3}
//address of a: 0xc000018260 &a[0]: 0xc000018260 | &a: 0xc00000c080 sh:0xc00000c080
結果發現a和&a[0]
地址相同。 這個好理解,切片指向地址即對應底層引用陣列首個元素地址
而&a和sh及sh.Data
指向地址相同。這個是因為這三個地址是指 slice 自身地址。 這裡【slice 自身地址不同於 slice 指向的底層資料結構地址】, 清楚這一點對於後邊的一些問題會更容易判斷。
這裡作為一個小插曲,我們看下當fmt.Printf("%p",a)
時發生了什麼
內部呼叫鏈 fmtPointer -> Value.Pointer
然後根據 Pointer 方法對應 slice 的註釋如下
// If v's Kind is Slice, the returned pointer is to the first
// element of the slice. If the slice is nil the returned value
// is 0. If the slice is empty but non-nil the return value is non-zero.
發現沒,正是我們上邊說的,slice 不為空時,返回了第一個元素的地址 有點迷惑性是不是,但其實作為使用 slice 的我們,更關心的是底層指向的資料不是麼。
再一點就是,基於 go 中所有賦值和引數傳遞都是值傳遞,對於大陣列而言,拷貝一個指向他的 slice 就高效多了 上一篇Go 之 for-range 排坑指南 有過描述, 詳見 0x03 對大陣列這樣遍歷有啥問題?
總結下, slice 是一個有底層陣列引用的結構裡,有長度,有容量。
就這麼簡單? 不,光這樣還不足以讓它比陣列更好用。 slice 還支援非常方便的切片操作和 append 時自動擴容,這讓他更加 flexible
0x02 slice 能比較麼?
答案是【只能和 nil 比較】
s := make([]int, 5)
a := s
println(s == a)
//invalid operation: s == a (slice can only be compared to nil)
這個也其實好理解,當你比較兩個 slice,你是想比較他們自身呢?(必然不同啊,因為有值拷貝) 還是比較他們底層的陣列?(那長度和容量也一起比較麼) 確實沒有什麼意義去做兩個 slice 的比較。
0x03 花樣的切片操作
slice 通過三段引數來操作:x[from:len:cap]
即對 x 從from
索引位置開始,擷取len
長度,cap
大小的新切片返回
但是 len 和 cap 不能大於 x 原來的 len 和 cap
三個引數都可省略,預設為x[0:len(x):cap(x)]
切片操作同樣適用於 array
如下都是通過src[:]
常規對切片(指向的底層陣列)或陣列的引用
s:=make([]int,5)
x:=s[:]
arr:=[5]int{}
y:=arr[:]
配合 copy 和 append,slice 的操作還有很多,官方 wikiSlice Tricks 有更豐富的例子
比如更通用的拷貝
b = append(a[:0:0], a...)
比如 cut 或 delete 時增加對不使用指標的 nil 標記釋放 (防止記憶體洩露)
//Cut
copy(a[i:], a[j:])
for k, n := len(a)-j+i, len(a); k < n; k++ {
a[k] = nil // or the zero value of T
}
a = a[:len(a)-j+i]
//Delete
if i < len(a)-1 {
copy(a[i:], a[i+1:])
}
a[len(a)-1] = nil // or the zero value of T
a = a[:len(a)-1]
不熟悉的話,建議好好練習一下去感受
0x04 append 時發生了什麼?
總的來說,append 時會按需自動擴容
- 容量足夠,無擴容則直接拷貝待 append 的資料到原 slice 底層指向的陣列 之後(原 slice 的 len 之後),並返回指向該陣列首地址的新 slice(len 改變)
- 容量不夠,有擴容則拷貝原有 slice 所指向部分資料到新開闢的陣列,並對待 append 的資料附加到其後,並返回新陣列首地址的新 slice(底層陣列,len,cap 均改變)
如下程式碼所示,容量不夠時觸發了擴容重新開闢底層陣列,x 和 s 底層指向的陣列已不是同一個
s := make([]int, 5)
x := append(s, 1)
fmt.Printf("x dataPtr: %p len: %d cap: %d\ns dataPtr: %p len: %d cap: %d",
x, len(x), cap(x),
s, len(s), cap(s))
// x dataPtr: 0xc000094000 len: 6 cap: 10
// s dataPtr: 0xc000092030 len: 5 cap: 5
0x05 append 內部優化
具體查閱原始碼,你會發現編譯時將 append 分為三類並優化
除按需擴容外
-
x = append(y, make([]T, y)...)
使用 memClr 提高初始化效率 -
x = append(l1, l2...)
或者x = append(slice, string)
直接複製 l2 -
x = append(src, a, b, c)
確定待 append 數目下,直接做賦值優化
具體編譯優化如下 註釋有簡化,詳見internal/gc/walk.go: append
switch {
case isAppendOfMake(r):
// x = append(y, make([]T, y)...) will rewrite to
// s := l1
// n := len(s) + l2
// if uint(n) > uint(cap(s)) {
// s = growslice(T, s, n)
// }
// s = s[:n]
// lptr := &l1[0]
// sptr := &s[0]
// if lptr == sptr || !hasPointers(T) {
// // growslice did not clear the whole underlying array
// (or did not get called)
// hp := &s[len(l1)]
// hn := l2 * sizeof(T)
// memclr(hp, hn)
// }
//使用memClr提高初始化效率
r = extendslice(r, init)
case r.IsDDD(): // DDD is ... syntax
// x = append(l1, l2...) will rewrite to
// s := l1
// n := len(s) + len(l2)
// if uint(n) > uint(cap(s)) {
// s = growslice(s, n)
// }
// s = s[:n]
// memmove(&s[len(l1)], &l2[0], len(l2)*sizeof(T))
//直接複製l2
r = appendslice(r, init) // also works for append(slice, string).
default:
// x = append(src, a, b, c) will rewrite to
// s := src
// const argc = len(args) - 1
// if cap(s) - len(s) < argc {
// s = growslice(s, len(s)+argc)
// }
// n := len(s)
// s = s[:n+argc]
// s[n] = a
// s[n+1] = b
// ...
//確定待append數目下,直接做賦值優化
r = walkappend(r, init, n)
}
這裡關於 append 實現有幾點可以提下
擴容的策略是什麼?
答案是【總的來說是至少返回要求的長度 n 最大則為翻倍】 具體情況是:
- len<1024 時 2 倍擴容
- 大於且未溢位時 1.25 倍擴容
- 溢位則直接按申請大小擴容
- 最後按
mallocgc
記憶體分配大小適配來確定 len. (n-2n 之間)
擴容留出最多一倍的餘量,主要還是為了減少可能的擴容頻率。 mallocgc 記憶體適配實際是 go 記憶體管理做了記憶體分配的優化, 當然內部也有記憶體對齊的考慮。 雨痕 Go 學習筆記第四章記憶體分配,對這一塊有很詳盡的分析,值得一讀。
至於為啥要記憶體對齊可以參見Golang 是否有必要記憶體對齊?,一篇不錯的文章。
擴容判斷中uint
的作用是啥?
//n為目標slice總長度,型別int,cap(s)型別也為int
if uint(n) > uint(cap(s))
s = growslice(T, s, n)
}
答案是【為了避免溢位的擴容】
int 有正負,最大值math.MaxInt64 = 1<<63 - 1
uint 無負數最大值math.MaxUint64 = 1<<64 - 1
uint 正值是 int 正值範圍的兩倍,int 溢位了變為負數,uint(n)
則必大於原 s 的 cap,條件成立
到 growslice 內部,對於負值的 n 會 panic,以此避免了溢位的擴容
記憶體清零初始化: memclrNoHeapPointers vs typedmemclr?
答案是【這個取決於待清零的記憶體是否已經初始化為 type-safe(型別安全)狀態,及型別是否包含指標】
具體來看,memclrNoHeapPointers
使用場景是
- 帶清零記憶體是初始化過的,且不含指標
- 帶清零記憶體未初始化過的,裡邊內容是 “垃圾值”(即非 type-safe),需要初始化並清零
其他場景就是typedmemclr
, 而且如果用於清零的 Type(型別) 包含指標,他會多一步 WriteBarrier(寫屏障),用於為 GC(垃圾回收) 執行時標記物件的記憶體修改,減少 STW(stop the world)
所以memclrNoHeapPointers
第一個使用場景為啥不含指標就不用解釋了。
想了解更多可以看看zero-initialization-versus-zeroing 以及相關原始碼的註釋memclrNoHeapPointers和typedmemclr
本文程式碼見 NewbMiao/Dig101-Go
歡迎關注,不定期挖點技術
文章首發:公眾號 newbmiao
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Dig101: Go之靈活的sliceGo
- Dig101:Go之string那些事Go
- Dig101: Go之for-range排坑指南Go
- go(golang)之slice的小想法1Golang
- Dig101:Go之讀懂map的底層設計Go
- Dig101:Go 之讀懂 map 的底層設計Go
- Dig101:Go 之聊聊 struct 的記憶體對齊GoStruct記憶體
- 030 Rust死靈書之讓Vec支援sliceRust
- 創業者需要的品質:靈活!靈活!靈活創業
- Go中的切片SliceGo
- go slice使用Go
- Go面試必考題目之slice篇Go面試
- Go 語言中的 切片 --sliceGo
- 《快學 Go 語言》第 5 課 —— 靈活的切片Go
- go map 和 sliceGo
- java靈活傳參之builder模式JavaUI模式
- GO 中 slice 的實現原理Go
- 原始碼分析:Phaser 之更靈活的同步屏障原始碼
- Go 切片 slice - Go 學習記錄Go
- Fiddler的靈活使用
- Go slice切片的“陷阱”和本質Go
- 【Go】slice的一些使用技巧Go
- 分析go中slice的奇怪現象Go
- Go語言slice的本質-SliceHeaderGoHeader
- Go 常見錯誤集錦之 append 操作 slice 時的副作用GoAPP
- 【Go語言基礎】sliceGo
- 【Go】深入剖析 slice 和 arrayGo
- 【Go】深入剖析slice和arrayGo
- 深度解密Go語言之Slice解密Go
- 陣列的靈活使用陣列
- Go 語言中的兩種 slice 表示式Go
- Go slice擴容分析之 不是double或1.25那麼簡單Go
- Go的100天之旅-06陣列和SliceGo陣列
- Go語言中切片slice的宣告與使用Go
- 詳解go語言的array和slice 【一】Go
- Go Quiz: 從Go面試題搞懂slice range遍歷的坑GoUI面試題
- 【Go進階—資料結構】sliceGo資料結構
- 兄弟連go教程(17)資料 - SliceGo