Go 堆棧的理解

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

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

數據結構的堆棧:程序員

堆:堆能夠被當作是一棵樹,如:堆排序。在隊列中,調度程序反覆提取隊列中第一個做業並運行,由於實際狀況中某些時間較短的任務將等待很長時間才能結束,或者某些不短小,但具備重要性的做業,一樣應當具備優先權。堆即爲解決此類問題設計的一種數據結構。github

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

這裏着重講的是內存分配中的堆和棧。算法

內存分配中的堆和棧數組

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

堆(操做系統): 通常由程序員分配釋放, 若程序員不釋放,程序結束時可能由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表明容量。這三個值表明第一個參數。

// 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"字符串的長度,他們倆做爲第二個參數。

第三個參數是一個整數,它是一個簡單的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。

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/blo...

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

https://golang.org/doc/faq

links

相關文章
相關標籤/搜索