Dig101:Go之靈活的slice

Dig101: dig more, simplified more and know more

Slice做爲go經常使用的數據類型,在平常編碼中很是常見。
相對於數組的定長不可變,slice使用起來就靈活了許多。android

0x01 slice 究竟是什麼?

首先咱們看下源碼中slice結構的定義git

// 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]的地址
看看他們的地址是否相同。github

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指向的底層數據結構地址】, 清楚這一點對於後邊的一些問題會更容易判斷。golang

<!--more-->segmentfault

這裏做爲一個小插曲,咱們看下當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 對大數組這樣遍歷有啥問題?session

總結下, slice是一個有底層數組引用的結構裏,有長度,有容量。數據結構

就這麼簡單? 不,光這樣還不足以讓它比數組更好用。
slice還支持很是方便的切片操做和append時自動擴容,這讓他更加flexibleapp

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
以及相關源碼的註釋memclrNoHeapPointerstypedmemclr

本文代碼見 NewbMiao/Dig101-Go

文章首發公衆號: newbmiao (歡迎關注,獲取及時更新內容)

推薦閱讀:Dig101-Go系列

newbmiao

相關文章
相關標籤/搜索