Loading

Golang逃逸分析变量在堆还是栈

Golang逃逸分析变量在堆还是栈

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出来的,会被分配到栈还是堆,是编译器经过逃逸分析之后决定的.

一般被外部引用的变量,一定会放入到堆内存中,如果编译器不能确定是否会被外部引用,那也会放入到堆内存中.

给一个引用对象中的引用类成员进行赋值,也会出现逃逸现象.

posted @ 2021-11-02 18:04  我爱吃炒鸡  阅读(78)  评论(0编辑  收藏  举报