Go 堆棧的理解

在講Go的堆棧之前,先溫習一下堆棧基礎知識。

什么是堆棧?在計算機中堆棧的概念分為:數據結構的堆棧和內存分配中堆棧。

數據結構的堆棧:

堆:堆可以被看成是一棵樹,如:堆排序。在隊列中,調度程序反復提取隊列中第一個作業并運行,因為實際情況中某些時間較短的任務將等待很長時間才能結束,或者某些不短小,但具有重要性的作業,同樣應當具有優先權。堆即為解決此類問題設計的一種數據結構。

棧:一種先進后出的數據結構。

這里著重講的是內存分配中的堆和棧。

內存分配中的堆和棧

棧(操作系統):由操作系統自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似于數據結構中的棧。

堆(操作系統): 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收,分配方式倒是類似于鏈表。

堆棧緩存方式

棧使用的是一級緩存, 他們通常都是被調用時處于存儲空間中,調用完畢立即釋放。

堆則是存放在二級緩存中,生命周期由虛擬機的垃圾回收算法來決定(并不是一旦成為孤兒對象就能被回收)。所以調用這些對象的速度要相對來得低一些。

堆棧跟蹤

下面討論堆棧跟蹤信息以及如何在堆棧中識別函數所傳遞的參數。

以下測試案例的版本是Go 1.11

示例:

package main

import "runtime/debug"

func main() {
   slice := make([]string, 2, 4)
   Example(slice, "hello", 10)
}
func Example(slice []string, str string, i int) {
   debug.PrintStack()
}

列表1是一個簡單的程序, main函數在第5行調用Example函數。Example函數在第9行聲明,它有三個參數,一個字符串slice,一個字符串和一個整數。它的方法體也很簡單,只有一行,debug.PrintStack(),這會立即產生一個堆棧跟蹤信息:

goroutine 1 [running]:
runtime/debug.Stack(0x1, 0x0, 0x0)
    C:/Go/src/runtime/debug/stack.go:24 +0xae
runtime/debug.PrintStack()
    C:/Go/src/runtime/debug/stack.go:16 +0x29
main.Example(0xc000077f48, 0x2, 0x4, 0x4abd9e, 0x5, 0xa)
    D:/gopath/src/example/example/main.go:10 +0x27
main.main()
    D:/gopath/src/example/example/main.go:7 +0x79

堆棧跟蹤信息:

第一行顯示運行的goroutine是id為 1的goroutine。

第二行 debug.Stack()被調用

第四行 debug.PrintStack() 被調用

第六行 調用debug.PrintStack()的代碼位置,位于main package下的Example函數。它也顯示了代碼所在的文件和路徑,以及debug.PrintStack()發生的行數(第10行)。

第八行 也調用Example的函數的名字,它是main package的main函數。它也顯示了文件名和路徑,以及調用Example函數的行數。

下面主要分析 傳遞Example函數傳參信息

// Declaration
main.Example(slice []string, str string, i int)
// Call to Example by main.
slice := make([]string, 2, 4)
Example(slice, "hello", 10)
// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

上面列舉了Example函數的聲明,調用以及傳遞給它的值的信息。當你比較函數的聲明以及傳遞的值時,發現它們并不一致。函數聲明只接收三個參數,而堆棧中卻顯示6個16進制表示的值。理解這一點的關鍵是要知道每個參數類型的實現機制。

讓我們看第一個[]string類型的參數。slice是引用類型,這意味著那個值是一個指針的頭信息(header value),它指向一個字符串。對于slice,它的頭是三個word數,指向一個數組。因此前三個值代表這個slice。

// Slice parameter value
slice := make([]string, 2, 4)
// Slice header values
Pointer:  0xc00006df48
Length:   0x2
Capacity: 0x4
// Declaration
main.Example(slice []string, str string, i int)
// Stack trace
main.Example(0xc00006df48, 0x2, 0x4, 0x4abd9e, 0x5, 0xa)

顯示了0xc00006df48代表第一個參數[]string的指針,0x2代表slice長度,0x4代表容量。這三個值代表第一個參數。

9.4.png

// String parameter value
“hello”
// String header values
Pointer: 0x4abd9e
Length:  0x5
// Declaration
main.Example(slice []string, str string, i int)
// Stack trace
main.Example(0xc00006df48, 0x2, 0x4, 0x4abd9e, 0x5, 0xa) 

顯示堆棧跟蹤信息中的第4個和第5個參數代表字符串的參數。0x4abd9e是指向這個字符串底層數組的指針,0x5是"hello"字符串的長度,他們倆作為第二個參數。

9.4.1.png

第三個參數是一個整數,它是一個簡單的word值。

// Integer parameter value
10
// Integer value
Base 16: 0xa
// Declaration
main.Example(slice []string, str string, i int)
// Stack trace
main.Example(0xc00006df48, 0x2, 0x4, 0x4abd9e, 0x5, 0xa) 

顯示堆棧中的最后一個參數就是Example聲明中的第三個參數,它的值是0xa,也就是整數10。

9.4.2.png

Methods

接下來讓我們稍微改動一下程序,讓Example變成方法。

package main

import (
   "fmt"
   "runtime/debug"
)

type trace struct{}

func main() {
   slice := make([]string, 2, 4)
   var t trace
   t.Example(slice, "hello", 10)
}
func (t *trace) Example(slice []string, str string, i int) {
   fmt.Printf("Receiver Address: %p\n", t)
   debug.PrintStack()
}

上例在第8行新增加了一個類型trace,在第15將example改變為trace的pointer receiver的一個方法。第12行聲明t的類型為trace,第13行調用它的方法。

因為這個方法聲明為pointer receiver的方法,Go使用t的指針來支持receiver type,即使代碼中使用值來調用這個方法。當程序運行時,堆棧跟蹤信息如下:

Receiver Address: 0x5781c8
goroutine 1 [running]:
runtime/debug.Stack(0x15, 0xc000071ef0, 0x1)
    C:/Go/src/runtime/debug/stack.go:24 +0xae
runtime/debug.PrintStack()
    C:/Go/src/runtime/debug/stack.go:16 +0x29
main.(*trace).Example(0x5781c8, 0xc000071f48, 0x2, 0x4, 0x4c04bb, 0x5, 0xa)
    D:/gopath/src/example/example/main.go:17 +0x7c
main.main()
    D:/gopath/src/example/example/main.go:13 +0x9a

第7行清晰的表明方法的receiver為pointer type。方法名和報包名中間有(*trace)。第二個值得注意的是堆棧信息中方法的第一個參數為receiver的值。方法調用總是轉換成函數調用,并將receiver的值作為函數的第一個參數。我們可以總堆棧信息中看到實現的細節。

Packing

import (
   "runtime/debug"
)

func main() {
   Example(true, false, true, 25)
}
func Example(b1, b2, b3 bool, i uint8) {

   debug.PrintStack()
}

再次改變Example的方法,讓它接收4個參數。前三個參數是布爾類型的,第四個參數是8bit無符號整數。布爾類型也是8bit表示的,所以這四個參數可以被打包成一個word,包括32位架構和64位架構。當程序運行的時候,會產生有趣的堆棧:

goroutine 1 [running]:
runtime/debug.Stack(0x4, 0xc00007a010, 0xc000077f88)
    C:/Go/src/runtime/debug/stack.go:24 +0xae
runtime/debug.PrintStack()
    C:/Go/src/runtime/debug/stack.go:16 +0x29
main.Example(0xc019010001)
    D:/gopath/src/example/example/main.go:12 +0x27
main.main()
    D:/gopath/src/example/example/main.go:8 +0x30

可以看到四個值被打包成一個單一的值了0xc019010001

// Parameter values
true, false, true, 25

// Word value
Bits    Binary      Hex   Value
00-07   0000 0001   01    true
08-15   0000 0000   00    false
16-23   0000 0001   01    true
24-31   0001 1001   19    25

// Declaration
main.Example(b1, b2, b3 bool, i uint8)

// Stack trace
main.Example(0x19010001)

顯示了堆棧的值如何和參數進行匹配的。true用1表示,占8bit, false用0表示,占8bit,uint8值25的16進制為x19,用8bit表示。我們課喲看到它們是如何表示成一個word值的。

Go運行時提供了詳細的信息來幫助我們調試程序。通過堆棧跟蹤信息stack trace,解碼傳遞個堆棧中的方法的參數有助于我們快速定位BUG。

變量是堆(heap)還是堆棧(stack)

寫過c語言都知道,有明確的堆棧和堆的相關概念。而Go聲明語法并沒有提到堆棧或堆,只是在Go的FAQ里面有這么一段解釋:

How do I know whether a variable is allocated on the heap or the stack?

From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

意思:從正確的角度來看,您不需要知道。Go中的每個變量都存在,只要有對它的引用即可。實現選擇的存儲位置與語言的語義無關。

存儲位置確實會影響編寫高效的程序。如果可能,Go編譯器將為該函數的堆棧幀中的函數分配本地變量。但是,如果編譯器在函數返回后無法證明變量未被引用,則編譯器必須在垃圾收集堆上分配變量以避免懸空指針錯誤。此外,如果局部變量非常大,將它存儲在堆而不是堆棧上可能更有意義。

在當前的編譯器中,如果變量具有其地址,則該變量是堆上分配的候選變量。但是,基本的轉義分析可以識別某些情況,這些變量不會超過函數的返回值并且可以駐留在堆棧上。

Go的編譯器會決定在哪(堆or棧)分配內存,保證程序的正確性。

下面通過反匯編查看具體內存分配情況:

新建 main.go

package main

import "fmt"

func main() {
   var a [1]int
   c := a[:]
   fmt.Println(c)
}

查看匯編代碼

go tool compile -S main.go

輸出:

[root@localhost example]# go tool compile -S main.go 
"".main STEXT size=183 args=0x0 locals=0x60
    0x0000 00000 (main.go:5)    TEXT    "".main(SB), $96-0
    0x0000 00000 (main.go:5)    MOVQ    (TLS), CX
    0x0009 00009 (main.go:5)    CMPQ    SP, 16(CX)
    0x000d 00013 (main.go:5)    JLS 173
    0x0013 00019 (main.go:5)    SUBQ    $96, SP
    0x0017 00023 (main.go:5)    MOVQ    BP, 88(SP)
    0x001c 00028 (main.go:5)    LEAQ    88(SP), BP
    0x0021 00033 (main.go:5)    FUNCDATA    $0, gclocals·f6bd6b3389b872033d462029172c8612(SB)
    0x0021 00033 (main.go:5)    FUNCDATA    $1, gclocals·3ea58e42e2dc6c51a9f33c0d03361a27(SB)
    0x0021 00033 (main.go:5)    FUNCDATA    $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
    0x0021 00033 (main.go:6)    PCDATA  $2, $1
    0x0021 00033 (main.go:6)    PCDATA  $0, $0
    0x0021 00033 (main.go:6)    LEAQ    type.[1]int(SB), AX
    0x0028 00040 (main.go:6)    PCDATA  $2, $0
    0x0028 00040 (main.go:6)    MOVQ    AX, (SP)
    0x002c 00044 (main.go:6)    CALL    runtime.newobject(SB)
    0x0031 00049 (main.go:6)    PCDATA  $2, $1
    0x0031 00049 (main.go:6)    MOVQ    8(SP), AX
    0x0036 00054 (main.go:8)    PCDATA  $2, $0
    0x0036 00054 (main.go:8)    PCDATA  $0, $1
    0x0036 00054 (main.go:8)    MOVQ    AX, ""..autotmp_4+64(SP)
。。。。。

注意到有調用newobject!其中main.go:6說明變量a的內存是在堆上分配的!

修改main.go

package main

func main() {
   var a [1]int
   c := a[:]
   println(c)
}

再查看匯編代碼

[root@localhost example]# go tool compile -S main.go 
\"".main STEXT size=102 args=0x0 locals=0x28
    0x0000 00000 (main.go:3)    TEXT    "".main(SB), $40-0
    0x0000 00000 (main.go:3)    MOVQ    (TLS), CX
    0x0009 00009 (main.go:3)    CMPQ    SP, 16(CX)
    0x000d 00013 (main.go:3)    JLS 95
    0x000f 00015 (main.go:3)    SUBQ    $40, SP
    0x0013 00019 (main.go:3)    MOVQ    BP, 32(SP)
    0x0018 00024 (main.go:3)    LEAQ    32(SP), BP
    0x001d 00029 (main.go:3)    FUNCDATA    $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x001d 00029 (main.go:3)    FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x001d 00029 (main.go:3)    FUNCDATA    $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
    0x001d 00029 (main.go:4)    PCDATA  $2, $0
    0x001d 00029 (main.go:4)    PCDATA  $0, $0
    0x001d 00029 (main.go:4)    MOVQ    $0, "".a+24(SP)
    0x0026 00038 (main.go:6)    CALL    runtime.printlock(SB)
    0x002b 00043 (main.go:6)    PCDATA  $2, $1
    0x002b 00043 (main.go:6)    LEAQ    "".a+24(SP), AX
    0x0030 00048 (main.go:6)    PCDATA  $2, $0
    0x0030 00048 (main.go:6)    MOVQ    AX, (SP)
    0x0034 00052 (main.go:6)    MOVQ    $1, 8(SP)
    0x003d 00061 (main.go:6)    MOVQ    $1, 16(SP)
    0x0046 00070 (main.go:6)    CALL    runtime.printslice(SB)
    0x004b 00075 (main.go:6)    CALL    runtime.printnl(SB)
    0x0050 00080 (main.go:6)    CALL    runtime.printunlock(SB)

沒有發現調用newobject,這段代碼a是在堆棧上分配的。

結論:

Go 編譯器自行決定變量分配在堆棧或堆上,以保證程序的正確性。

參考資料:

https://www.ardanlabs.com/blog/2015/01/stack-traces-in-go.html

https://zhuanlan.zhihu.com/p/28484133

https://golang.org/doc/faq

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容