神奇的現象
切片,切片,又是切片!
前一篇文章切片真的是引用類型嘛?講的是切片, 今天遇到的神奇問題還是和切片有關, 具體怎麼個神奇法, 讓我們來看看下面幾個現象
現象一
現象二
現象三
現象四
現象五
分析
到這兒我已經滿腦子問號了
字符串變量轉切片
一個小小的字符串轉切片, 內部究竟發生了什麼, 竟然如此的神奇。 這種時候只好祭出前一篇文章的套路了, 看看彙編代碼(希望之後有機會能夠對go的彙編語法進行簡單的介紹)有沒有什麼關鍵詞能夠幫助我們
以下為現象一轉換的彙編代碼關鍵部分
以下為現象二轉換的彙編代碼關鍵部分
在看彙編代碼之前, 我們首先來看一看runtime.stringtoslicebyte的函數籤名
到這裡只靠關鍵詞已經無法看出更多的信息了,還是需要稍微了解一下彙編的語法,筆者在這裡列出一點簡單的分析, 之後我們還是可以通過取巧的方法發現更多的東西
通過上面彙編代碼的分析可以知道,現象一和現象二的區別就是傳遞給runtime.stringtoslicebyte的第一個參數不同。 通過對runtime包中stringtoslicebyte函數分析,第一個參數是否有值和字符串長度會影響代碼執行的分支,從而生成不同的切片, 因此容量不一樣也是常理之中, 下面我們看源碼
然而, stringtoslicebyte的第一個參數什麼情況下才會有值,什麼情況下為nil, 我們仍然不清楚。那怎麼辦呢, 只好祭出全局搜索大法:
最終在go的編譯器源碼cmd/compile/internal/gc/walk.go發現了如下代碼塊
我們查看mkcall函數籤名可以知道, 從第四個參數開始的所有變量都會作為參數傳遞給第一個參數對應的函數, 最後生成一個*Node的變量。其中Node結構體解釋如下:
綜合上述信息我們得出的結論是,編譯器會對stringtoslicebyte的函數調用生成一個AST(抽象語法樹)對應的節點。因此我們也知道傳遞給stringtoslicebyte函數的第一個變量也就對應於上圖中的變量a.
其中a的初始值為nodnil()的返回值,即默認為nil. 但是n.Esc == EscNone時,a會變成一個數組。我們看一下EscNone的解釋.
由上可知, EscNone用來判斷變量是否逃逸,到這兒了我們就很好辦了,接下來我們對現象一和現象二的代碼進行逃逸分析.
根據上面的信息我們知道在現象一中,bs變量發生了逃逸,現象二中變量未發生逃逸,也就是說stringtoslicebyte函數的第一個參數在變量未發生逃逸時其值不為nil,變量發生逃逸時其值為nil。到這裡我們已經搞明白stringtoslicebyte的第一個參數了, 那我們繼續分析stringtoslicebyte的內部邏輯
我們在runtime/string.go中看到stringtoslicebyte第一個參數的類型定義如下:
綜上: 現象二中bs變量未發生變量逃逸, stringtoslicebyte第一個參數不為空且是一個長度為32的byte數組, 因此在現象二中生成了一個容量為32的切片
根據對stringtoslicebyte的源碼分析, 我們知道現象一調用了rawbyteslice函數
由上面的代碼知道, 切片的容量通過runtime/msize.go中的roundupsize函數計算得出, 其中_MaxSmallSize和class_to_size均定義在runtime/sizeclasses.go
由於字符串abc的長度小於_MaxSmallSize(32768),故意切片的長度只能取數組class_to_size中的值, 即0, 8, 16, 32, 48, 64, 80, 96, 112, 128....
至此, 現象一中切片容量為什麼為8也真相大白了。相信到這裡很多人已經明白現象四和現象五是怎麼回事兒了, 其邏輯分別與現象一和現象二是一致的, 有興趣的, 可以在自己的電腦上面試一試。
字符串直接轉切片
相信有人問了,你說了這麼多, 現象三還是不能解釋啊。請各位看官莫急, 接下來我們繼續分析。
各位細心的小夥伴應該早就發現了我們在上面的cmd/compile/internal/gc/walk.go
源碼圖中摺疊了部分代碼, 現在我們就將這塊神秘的代碼赤裸裸的展示出來
我們分析這塊代碼發現,go編譯器在將字符串轉字節切片生成AST時,總共分為三步。
先判斷該變量是否是常量字符串,如果是常量字符串,則直接通過types.NewArray創建一個和字符串等長的數組常量字符串生成的切片變量也要進行逃逸分析,並判斷其大小是否大於函數棧允許分配給變量的最大長度, 從而判斷節點是分配在棧上還是在堆上最後,如果字符串長度是大於0, 將字符串內容複製到字節切片中, 然後返回。因此現象三中的切片容量是3也就完全清楚了結論
字符串轉字節切片步驟如下
判斷是否是常量, 如果是常量則轉換為等容量等長的字節切片如果是變量, 先判斷生成的切片是否發生變量逃逸如果逃逸或者字符串長度>32, 則根據字符串長度可以計算出不同的容量如果未逃逸且字符串長度<=32, 則字符切片容量為32擴展
常見逃逸情況
函數返回局部指針棧空間不足逃逸動態類型逃逸, 很多函數參數為interface類型,比如fmt.Println(a ...interface{}),編譯期間很難確定其參數的具體類型, 也會發生逃逸閉包引用對象逃逸注: 寫本文時, 筆者所用go版本為: go1.13.4
參考
https://golang.org/src/cmd/compile/README.md
https://my.oschina.net/renhc/blog/2222104
https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/walk.go#L1522
https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/esc.go#L39
https://github.com/golang/go/blob/master/src/runtime/string.go#L1522
https://github.com/golang/go/blob/master/src/runtime/msize.go
https://github.com/golang/go/blob/master/src/runtime/sizeclasses.go#L75
生命不息, 探索不止, 後續將持續更新有關於go的技術探索
原創不易, 卑微求關注收藏二連.