<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                [TOC] ## 3、Golang中逃逸現象, 變量“何時棧?何時堆?” ### 一、C/C++報錯?Golang通過? 我們先看一段代碼 ```go package main func foo(arg_val int)(*int) { var foo_val int = 11; return &foo_val; } func main() { main_val := foo(666) println(*main_val) } ``` 編譯運行 ```bash $ go run pro_1.go 11 ``` 竟然沒有報錯! 了解C/C++的小伙伴應該知道,這種情況是一定不允許的,因為 外部函數使用了子函數的局部變量, 理論來說,子函數的`foo_val` 的聲明周期早就銷毀了才對,如下面的C/C++代碼 ```c #include <stdio.h> int *foo(int arg_val) { int foo_val = 11; return &foo_val; } int main() { int *main_val = foo(666); printf("%d\n", *main_val); } ``` 編譯 ```bash $ gcc pro_1.c pro_1.c: In function ‘foo’: pro_1.c:7:12: warning: function returns address of local variable [-Wreturn-local-addr] return &foo_val; ^~~~~~~~ ``` 出了一個警告,不管他,再運行 ``` $ ./a.out 段錯誤 (核心已轉儲) ``` 程序崩潰. 如上C/C++編譯器明確給出了警告,foo把一個局部變量的地址返回了;反而高大上的go沒有給出任何警告,難道是go編譯器識別不出這個問題嗎? ### 二、Golang編譯器得逃逸分析 ? go語言編譯器會自動決定把一個變量放在棧還是放在堆,編譯器會做**逃逸分析(escape analysis)**,**當發現變量的作用域沒有跑出函數范圍,就可以在棧上,反之則必須分配在堆**。 go語言聲稱這樣可以釋放程序員關于內存的使用限制,更多的讓程序員關注于程序功能邏輯本身。 我們再看如下代碼: ```go package main func foo(arg_val int) (*int) { var foo_val1 int = 11; var foo_val2 int = 12; var foo_val3 int = 13; var foo_val4 int = 14; var foo_val5 int = 15; //此處循環是防止go編譯器將foo優化成inline(內聯函數) //如果是內聯函數,main調用foo將是原地展開,所以foo_val1-5相當于main作用域的變量 //即使foo_val3發生逃逸,地址與其他也是連續的 for i := 0; i < 5; i++ { println(&arg_val, &foo_val1, &foo_val2, &foo_val3, &foo_val4, &foo_val5) } //返回foo_val3給main函數 return &foo_val3; } func main() { main_val := foo(666) println(*main_val, main_val) } ``` 我們運行一下 ```bash $ go run pro_2.go 0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720 0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720 0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720 0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720 0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720 13 0xc000082000 ``` 我們能看到`foo_val3`是返回給main的局部變量, 其中他的地址應該是`0xc000082000`,很明顯與其他的foo_val1、2、3、4不是連續的. 我們用`go tool compile`測試一下 ```bash $ go tool compile -m pro_2.go pro_2.go:24:6: can inline main pro_2.go:7:9: moved to heap: foo_val3 ``` 果然,在編譯的時候, `foo_val3`具有被編譯器判定為逃逸變量, 將`foo_val3`放在堆中開辟. 我們在用匯編證實一下: ```bash $ go tool compile -S pro_2.go > pro_2.S ``` 打開pro_2.S文件, 搜索`runtime.newobject`關鍵字 ```go ... 16 0x0021 00033 (pro_2.go:5) PCDATA $0, $0 17 0x0021 00033 (pro_2.go:5) PCDATA $1, $0 18 0x0021 00033 (pro_2.go:5) MOVQ $11, "".foo_val1+48(SP) 19 0x002a 00042 (pro_2.go:6) MOVQ $12, "".foo_val2+40(SP) 20 0x0033 00051 (pro_2.go:7) PCDATA $0, $1 21 0x0033 00051 (pro_2.go:7) LEAQ type.int(SB), AX 22 0x003a 00058 (pro_2.go:7) PCDATA $0, $0 23 0x003a 00058 (pro_2.go:7) MOVQ AX, (SP) 24 0x003e 00062 (pro_2.go:7) CALL runtime.newobject(SB) //foo_val3是被new出來的 25 0x0043 00067 (pro_2.go:7) PCDATA $0, $1 26 0x0043 00067 (pro_2.go:7) MOVQ 8(SP), AX 27 0x0048 00072 (pro_2.go:7) PCDATA $1, $1 28 0x0048 00072 (pro_2.go:7) MOVQ AX, "".&foo_val3+56(SP) 29 0x004d 00077 (pro_2.go:7) MOVQ $13, (AX) 30 0x0054 00084 (pro_2.go:8) MOVQ $14, "".foo_val4+32(SP) 31 0x005d 00093 (pro_2.go:9) MOVQ $15, "".foo_val5+24(SP) 32 0x0066 00102 (pro_2.go:9) XORL CX, CX 33 0x0068 00104 (pro_2.go:15) JMP 252 ... ``` 看出來, foo_val3是被runtime.newobject()在堆空間開辟的, 而不是像其他幾個是基于地址偏移的開辟的棧空間. ### 三、new的變量在棧還是堆? 那么對于new出來的變量,是一定在heap中開辟的嗎,我們來看看 ```go package main func foo(arg_val int) (*int) { var foo_val1 * int = new(int); var foo_val2 * int = new(int); var foo_val3 * int = new(int); var foo_val4 * int = new(int); var foo_val5 * int = new(int); //此處循環是防止go編譯器將foo優化成inline(內聯函數) //如果是內聯函數,main調用foo將是原地展開,所以foo_val1-5相當于main作用域的變量 //即使foo_val3發生逃逸,地址與其他也是連續的 for i := 0; i < 5; i++ { println(arg_val, foo_val1, foo_val2, foo_val3, foo_val4, foo_val5) } //返回foo_val3給main函數 return foo_val3; } func main() { main_val := foo(666) println(*main_val, main_val) } ``` 我們將foo_val1-5全部用new的方式來開辟, 編譯運行看結果 ```bash $ go run pro_3.go 666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730 666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730 666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730 666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730 666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730 0 0xc00001a0e0 ``` 很明顯, `foo_val3`的地址`0xc00001a0e0 `依然與其他的不是連續的. 依然具備逃逸行為. ### 四、逃逸規則 我們其實都知道一個普遍的規則,就是如果變量需要使用堆空間,那么他就應該進行逃逸。但是實際上Golang并不僅僅把逃逸的規則如此泛泛。Golang會有很多場景具備出現逃逸的現象。 一般我們給一個引用類對象中的引用類成員進行賦值,可能出現逃逸現象。可以理解為訪問一個引用對象實際上底層就是通過一個指針來間接的訪問了,但如果再訪問里面的引用成員就會有第二次間接訪問,這樣操作這部分對象的話,極大可能會出現逃逸的現象。 Go語言中的引用類型有func(函數類型),interface(接口類型),slice(切片類型),map(字典類型),channel(管道類型),\*(指針類型)等。 那么我們下面的一些操作場景是產生逃逸的。 #### 逃逸范例一 `[]interface{}`數據類型,通過`[]`賦值必定會出現逃逸。 ```go package main func main() { data := []interface{}{100, 200} data[0] = 100 } ``` 我們通過編譯看看逃逸結果 ```bash aceld:test ldb$ go tool compile -m 1.go 1.go:3:6: can inline main 1.go:4:23: []interface {}{...} does not escape 1.go:4:24: 100 does not escape 1.go:4:29: 200 does not escape 1.go:6:10: 100 escapes to heap ``` 我們能看到,`data[0] = 100` 發生了逃逸現象。 #### 逃逸范例二 `map[string]interface{}`類型嘗試通過賦值,必定會出現逃逸。 ```go package main func main() { data := make(map[string]interface{}) data["key"] = 200 } ``` 我們通過編譯看看逃逸結果 ```go aceld:test ldb$ go tool compile -m 2.go 2.go:3:6: can inline main 2.go:4:14: make(map[string]interface {}) does not escape 2.go:6:14: 200 escapes to heap ``` 我們能看到,`data["key"] = 200` 發生了逃逸。 #### 逃逸范例三 `map[interface{}]interface{}`類型嘗試通過賦值,會導致key和value的賦值,出現逃逸。 ```go package main func main() { data := make(map[interface{}]interface{}) data[100] = 200 } ``` 我們通過編譯看看逃逸結果 ```go aceld:test ldb$ go tool compile -m 3.go 3.go:3:6: can inline main 3.go:4:14: make(map[interface {}]interface {}) does not escape 3.go:6:6: 100 escapes to heap 3.go:6:12: 200 escapes to heap ``` 我們能看到,`data[100] = 200` 中,100和200均發生了逃逸。 #### 逃逸范例四 `map[string][]string`數據類型,賦值會發生`[]string`發生逃逸。 ```go package main func main() { data := make(map[string][]string) data["key"] = []string{"value"} } ``` 我們通過編譯看看逃逸結果 ```bash aceld:test ldb$ go tool compile -m 4.go 4.go:3:6: can inline main 4.go:4:14: make(map[string][]string) does not escape 4.go:6:24: []string{...} escapes to heap ``` 我們能看到,`[]string{...}`切片發生了逃逸。 #### 逃逸范例五 `[]*int`數據類型,賦值的右值會發生逃逸現象。 ```go package main func main() { a := 10 data := []*int{nil} data[0] = &a } ``` 我們通過編譯看看逃逸結果 ```bash aceld:test ldb$ go tool compile -m 5.go 5.go:3:6: can inline main 5.go:4:2: moved to heap: a 5.go:6:16: []*int{...} does not escape ``` 其中 `moved to heap: a`,最終將變量a 移動到了堆上。 #### 逃逸范例六 `func(*int)`函數類型,進行函數賦值,會使傳遞的形參出現逃逸現象。 ```go package main import "fmt" func foo(a *int) { return } func main() { data := 10 f := foo f(&data) fmt.Println(data) } ``` 我們通過編譯看看逃逸結果 ```bash aceld:test ldb$ go tool compile -m 6.go 6.go:5:6: can inline foo 6.go:12:3: inlining call to foo 6.go:14:13: inlining call to fmt.Println 6.go:5:10: a does not escape 6.go:14:13: data escapes to heap 6.go:14:13: []interface {}{...} does not escape :1: .this does not escape ``` 我們會看到data已經被逃逸到堆上。 #### 逃逸范例七 * `func([]string)`: 函數類型,進行`[]string{"value"}`賦值,會使傳遞的參數出現逃逸現象。 ```go package main import "fmt" func foo(a []string) { return } func main() { s := []string{"aceld"} foo(s) fmt.Println(s) } ``` 我們通過編譯看看逃逸結果 ```bash aceld:test ldb$ go tool compile -m 7.go 7.go:5:6: can inline foo 7.go:11:5: inlining call to foo 7.go:13:13: inlining call to fmt.Println 7.go:5:10: a does not escape 7.go:10:15: []string{...} escapes to heap 7.go:13:13: s escapes to heap 7.go:13:13: []interface {}{...} does not escape :1: .this does not escape ``` 我們看到 `s escapes to heap`,s被逃逸到堆上。 #### 逃逸范例八 * `chan []string`數據類型,想當前channel中傳輸`[]string{"value"}`會發生逃逸現象。 ```go package main func main() { ch := make(chan []string) s := []string{"aceld"} go func() { ch <- s }() } ``` 我們通過編譯看看逃逸結果 ```bash aceld:test ldb$ go tool compile -m 8.go 8.go:8:5: can inline main.func1 8.go:6:15: []string{...} escapes to heap 8.go:8:5: func literal escapes to heap ``` 我們看到` []string{...} escapes to heap`, s被逃逸到堆上。 ### 五、結論 Golang中一個函數內局部變量,不管是不是動態new出來的,它會被分配在堆還是棧,是由編譯器做逃逸分析之后做出的決定。 按理來說, 人家go的設計者明明就不希望開發者管這些,但是面試官就偏偏找這種問題問? 醉了也是.
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看