1. 程式人生 > 其它 >Go逃逸分析變數在堆還是棧

Go逃逸分析變數在堆還是棧

目錄

Go逃逸分析變數在堆還是棧

References

https://www.youtube.com/watch?v=3D4o0MVs4Qo

https://www.kancloud.cn/aceld/golang/1958306

https://u.geekbang.org/subject/go?utm_identify=geektime&gk_cus_user_wechat=university

堆疊記憶體

​ 在計算機體系中,堆heap,棧stack 是記憶體中用於存放程式資料而抽象出的區域.

  • 棧的優點是存取速度更快,缺點是是缺乏靈活性; 棧中資料的大小和生命週期是確定的,與函式一致.一般用來存放函式引數和區域性變數,由編譯器自動分配和釋放.
  • 堆的優點是可以動態分配記憶體大小,適合不可預知大小的記憶體分配,生命週期享有自主權,缺點是複雜的記憶體分配管理會佔用資源,速度慢,而且會出現記憶體碎片以及堆記憶體洩漏問題.

逃逸分析Escape Analysis

根據堆和棧的不同特點,需要對程式中的資料選擇合適的區域進行存放.

例如在C/C++中對記憶體的管理,需要程式設計師開發者自行管理.

現代的大部分語言將複雜的易出錯的記憶體管理交給編譯器解析器來管理,將堆疊記憶體分配對程式設計師透明,降低開發負擔.

其中比較優秀的技術就是逃逸分析,編譯器能分析程式碼特徵,決定資料使用堆還是棧.顧名思義是將本應在棧存放的資料逃逸到堆中存放.

每種語言的逃逸分析技術的實現都大同小異,Java也可以通過JVM引數開啟逃逸分析.

程式碼案例

C程式碼的逃逸案例

在C語言中是沒有逃逸分析的,所以這裡會報錯.出現野指標

#include<stdio.h>

int* returnAddr()
{
	//函式結束後棧記憶體會被釋放,這裡返回的佔記憶體中的指標就會成為野指標
	int a = 3;
	return &a; //返回區域性變數的記憶體地址
}

int main()
{
	return 0;
}

執行結果,報錯:禁止之返回區域性變數

test.cc:7:10: warning: address of stack memory associated with local variable 'a' returned [-Wreturn-stack-address]
        return &a; //返回區域性變數的記憶體地址
                ^
1 warning generated.

Go程式碼案例

逃逸分析

程式碼可以正常執行

go編譯器會自行決定變數儲存在堆還是棧上,就是逃逸分析escape analysis

如果變數的作用域沒有跑出函式範圍,就可以在棧上,否則分配在堆.

package main

//go build -gcflags '-m -l' ./main.go //列印逃逸分析
//go tool compile -S ./main.go  //列印彙編程式碼
//go tool compile -m ./main.go  //列印逃逸分析

func returnAddr() *int {
	//會出現逃逸到堆記憶體中
	a := 1
	return &a //返回區域性變數的指標
}

func returnAddr1() int {
	//不會逃逸到堆記憶體中
	a := new(int)
	return *a //返回區域性變數指標的值
}

func main() {
	returnAddr()
	returnAddr1()
}

執行結果分析

# 可以看到這裡提示第8行有將變數移動到堆中(moved to heap: a)

$ go build -gcflags '-m -l' ./main.go
# command-line-arguments
./main.go:8:2: moved to heap: a
./main.go:14:10: new(int) does not escape

將彙編列表列印到標準輸出

# 搜尋runtime.newobject關鍵字 在堆空間的
$ go tool compile -S main.go
"".returnAddr STEXT size=79 args=0x8 locals=0x18 funcid=0x0
	0x0000 00000 (main.go:6)	TEXT	"".returnAddr(SB), ABIInternal, $24-8
	0x0000 00000 (main.go:6)	MOVQ	(TLS), CX
	0x0009 00009 (main.go:6)	CMPQ	SP, 16(CX)
	0x000d 00013 (main.go:6)	PCDATA	$0, $-2
	0x000d 00013 (main.go:6)	JLS	72
	0x000f 00015 (main.go:6)	PCDATA	$0, $-1
	0x000f 00015 (main.go:6)	SUBQ	$24, SP
	0x0013 00019 (main.go:6)	MOVQ	BP, 16(SP)
	0x0018 00024 (main.go:6)	LEAQ	16(SP), BP
	0x001d 00029 (main.go:6)	FUNCDATA	$0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
	0x001d 00029 (main.go:6)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (main.go:8)	LEAQ	type.int(SB), AX
	0x0024 00036 (main.go:8)	MOVQ	AX, (SP)
	0x0028 00040 (main.go:8)	PCDATA	$1, $0
	0x0028 00040 (main.go:8)	CALL	runtime.newobject(SB)
	0x002d 00045 (main.go:8)	MOVQ	8(SP), AX
	0x0032 00050 (main.go:8)	MOVQ	$1, (AX)
	0x0039 00057 (main.go:9)	MOVQ	AX, "".~r0+32(SP)
	0x003e 00062 (main.go:9)	MOVQ	16(SP), BP
	0x0043 00067 (main.go:9)	ADDQ	$24, SP
	0x0047 00071 (main.go:9)	RET
	0x0048 00072 (main.go:9)	NOP
	0x0048 00072 (main.go:6)	PCDATA	$1, $-1
	0x0048 00072 (main.go:6)	PCDATA	$0, $-2
	0x0048 00072 (main.go:6)	CALL	runtime.morestack_noctxt(SB)
	0x004d 00077 (main.go:6)	PCDATA	$0, $-1
	0x004d 00077 (main.go:6)	JMP	0
	0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 39 48  eH..%....H;a.v9H
	0x0010 83 ec 18 48 89 6c 24 10 48 8d 6c 24 10 48 8d 05  ...H.l$.H.l$.H..
	0x0020 00 00 00 00 48 89 04 24 e8 00 00 00 00 48 8b 44  ....H..$.....H.D
	0x0030 24 08 48 c7 00 01 00 00 00 48 89 44 24 20 48 8b  $.H......H.D$ H.
	0x0040 6c 24 10 48 83 c4 18 c3 e8 00 00 00 00 eb b1     l$.H...........
	rel 5+4 t=17 TLS+0
	rel 32+4 t=16 type.int+0
	rel 41+4 t=8 runtime.newobject+0
	rel 73+4 t=8 runtime.morestack_noctxt+0
"".returnAddr1 STEXT nosplit size=10 args=0x8 locals=0x0 funcid=0x0
	0x0000 00000 (main.go:12)	TEXT	"".returnAddr1(SB), NOSPLIT|ABIInternal, $0-8
	0x0000 00000 (main.go:12)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (main.go:12)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (main.go:15)	MOVQ	$0, "".~r0+8(SP)
	0x0009 00009 (main.go:15)	RET
	0x0000 48 c7 44 24 08 00 00 00 00 c3                    H.D$......
"".main STEXT nosplit size=1 args=0x0 locals=0x0 funcid=0x0
	0x0000 00000 (main.go:18)	TEXT	"".main(SB), NOSPLIT|ABIInternal, $0-0
	0x0000 00000 (main.go:18)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (main.go:18)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (main.go:20)	RET
	0x0000 c3                                               .
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
	0x0000 6d 61 69 6e                                      main
""..inittask SNOPTRDATA size=24
	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 00 00 00 00 00 00 00 00                          ........
go.info."".returnAddr$abstract SDWARFABSFCN dupok size=24
	0x0000 04 2e 72 65 74 75 72 6e 41 64 64 72 00 01 01 0c  ..returnAddr....
	0x0010 61 00 08 00 00 00 00 00                          a.......
	rel 0+0 t=24 type.*int+0
	rel 0+0 t=24 type.int+0
	rel 19+4 t=31 go.info.int+0
go.info."".returnAddr1$abstract SDWARFABSFCN dupok size=25
	0x0000 04 2e 72 65 74 75 72 6e 41 64 64 72 31 00 01 01  ..returnAddr1...
	0x0010 0c 61 00 0e 00 00 00 00 00                       .a.......
	rel 0+0 t=24 type.*int+0
	rel 0+0 t=24 type.int+0
	rel 20+4 t=31 go.info.*int+0
gclocals·2a5305abe05176240e61b8620e19a815 SRODATA dupok size=9
	0x0000 01 00 00 00 01 00 00 00 00                       .........
gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
	0x0000 01 00 00 00 00 00 00 00                          ........

逃逸案例

除了上面的返回區域性指標之外,給一個引用物件中的引用類成員進行賦值,也會出現逃逸現象.

可以理解為當訪問一個引用物件時,實際上底層就是通過一個指標來間接的訪問,但如果再訪問裡面的引用成員就會有第二次間接訪問,這樣操作這部分物件的,極大可能會出現逃逸的現象.

Go的引用型別有: func,interface,slice,map,channel,指標.

案例彙總

package main

import (
	"log"
)

func demo(a *int) {
	return
}

func demo1(a []string) {
	return
}

func main() {
	/**
	  逃逸案例
	*/
	//案例1
	//[]interface{}資料型別,通過[]賦值必定會出現逃逸
	data1 := []interface{}{100, 200}
	data1[0] = 100

	//案例2
	//map[string]interface{}型別嘗試通過賦值,必定會出現逃逸
	data2 := make(map[string]interface{})
	data2["key"] = 200

	//案例3
	//map[interface{}]interface{}型別嘗試通過賦值,會導致key和value的賦值,出現逃逸
	data3 := make(map[interface{}]interface{})
	data3[100] = 200

	//案例4
	//map[string][]string資料型別,賦值會發生[]string發生逃逸
	data4 := make(map[string][]string)
	data4["key"] = []string{"value"}

	//案例5
	//[]*int資料型別,賦值的右值會發生逃逸現象
	a := 10
	data5 := []*int{nil}
	data5[0] = &a

	//案例6
	//func(*int)函式型別,進行函式賦值,會使傳遞的形參出現逃逸現象
	data6 := 10
	f := demo
	f(&data6)
	log.Println(data6)

	//案例7
	//func([]string):函式型別,進行[]string{"value"}賦值,會使傳遞的引數出現逃逸現象
	s := []string{"aceld"}
	demo1(s)
	log.Println(s)

	//案例8
	//chan []string資料型別,想當前channel中傳輸[]string{"value"}會發生逃逸現象
	ch := make(chan []string)
	s1 := []string{"aceld"}
	go func() {
		ch <- s1
	}()

	//案例9
	//函式返回區域性指標,會出現逃逸
	func() *int {
		a := 1
		return &a
	}()
}

執行逃逸分析的結果

$ go tool compile -m main.go
main.go:7:6: can inline demo
main.go:11:6: can inline demo1
main.go:49:3: inlining call to demo
main.go:55:7: inlining call to demo1
main.go:62:5: can inline main.func1
main.go:7:11: a does not escape
main.go:11:12: a does not escape
main.go:41:2: moved to heap: a
main.go:21:24: []interface {}{...} does not escape
main.go:21:25: 100 does not escape
main.go:21:30: 200 does not escape
main.go:22:11: 100 escapes to heap
main.go:26:15: make(map[string]interface {}) does not escape
main.go:27:15: 200 escapes to heap
main.go:31:15: make(map[interface {}]interface {}) does not escape
main.go:32:7: 100 escapes to heap
main.go:32:13: 200 escapes to heap
main.go:36:15: make(map[string][]string) does not escape
main.go:37:25: []string{...} escapes to heap
main.go:42:17: []*int{...} does not escape
main.go:50:13: ... argument does not escape
main.go:50:13: data6 escapes to heap
main.go:54:15: []string{...} escapes to heap
main.go:56:13: ... argument does not escape
main.go:56:13: s escapes to heap
main.go:61:16: []string{...} escapes to heap
main.go:62:5: func literal escapes to heap

逃逸案例一

[]interface{}資料型別,通過[]賦值必定會出現逃逸

package main

func main() {
    data := []interface{}{100, 200}
    data[0] = 100//逃逸
}

逃逸案例二

map[string]interface{}型別嘗試通過賦值,必定會出現逃逸

package main

func main() {
    data := make(map[string]interface{})
    data["key"] = 200//逃逸
}

逃逸案例三

map[interface{}]interface{}型別嘗試通過賦值,會導致key和value的賦值,出現逃逸

package main

func main() {
    data := make(map[interface{}]interface{})
    data[100] = 200//100和200均發生了逃逸
}

逃逸案例四

map[string][]string資料型別,賦值會發生[]string發生逃逸

package main

func main() {
    data := make(map[string][]string)//會逃逸到堆上
    data["key"] = []string{"value"}
}

逃逸案例五

[]*int資料型別,賦值的右值會發生逃逸現象

package main

func main() {
    a := 10//逃逸
    data := []*int{nil}
    data[0] = &a
}

逃逸案例六

func(*int)函式型別,進行函式賦值,會使傳遞的形參出現逃逸現象

package main

import "fmt"

func foo(a *int) {
    return
}

func main() {
    data := 10//逃逸
    f := foo
    f(&data)
    fmt.Println(data)
}

逃逸案例七

func([]string): 函式型別,進行[]string{"value"}賦值,會使傳遞的引數出現逃逸現象

package main

import "fmt"

func foo(a []string) {
    return
}

func main() {
    s := []string{"aceld"}//逃逸
    foo(s)
    fmt.Println(s)
}

逃逸案例八

chan []string資料型別,想當前channel中傳輸[]string{"value"}會發生逃逸現象

package main

func main() {
    ch := make(chan []string)

    s := []string{"aceld"}//逃逸

    go func() {
        ch <- s
    }()
}

總結

也就是說,函式內的區域性變數,不論是否是動態new出來的,會被分配到棧還是堆,是編譯器經過逃逸分析之後決定的.

一般被外部引用的變數,一定會放入到堆記憶體中,如果編譯器不能確定是否會被外部引用,那也會放入到堆記憶體中.

給一個引用物件中的引用類成員進行賦值,也會出現逃逸現象.

本文來自部落格園,作者:我愛吃炒雞,轉載請註明原文連結:https://www.cnblogs.com/chinaliuhan/p/15500255.html