Go —— 逃逸分析

本文探讨了逃逸分析在编程中的作用,包括决定内存分配位置、指针逃逸、栈空间限制和动态类型的处理。通过实例展示了如何通过编译器分析确定逃逸行为,并讨论了传递指针效率的问题。
摘要由CSDN通过智能技术生成

逃逸分析

  逃逸分析是指由编译器决定内存分配的位置,不需要程序员指定。在函数中申请一个新的对象:

  • 如果分配在栈中,则函数执行结束后可自动将内存回收
  • 如果分配在堆中,则函数执行结束后可交给GC(垃圾回收)处理

  有了逃逸分析,返回函数局部变量变得可能。除此之外,逃逸分析还跟闭包息息相关,了解哪些场景下对象会逃逸至关重要。

1. 逃逸策略

  在函数中申请新的对象时,编译器会根据该对象是否被函数外部引用来决定是否逃逸:

  • 如果函数外部没有引用,则优先放到栈中
  • 如果函数外部存在引用,则必定放在堆中

对于仅在函数内部使用的变量,也有可能放到堆中,比如内存过大超过栈的存储能力。

2. 逃逸场景

1)指针逃逸

  Go可以返回局部变量指针,这是一个典型的变量逃逸案例。示例代码如下:

package main

type Student struct {
	Name string
	Age  int
}

func StudentRegister(name string, age int) *Student {
	s := new(Student)
	s.Name = name
	s.Age = age
	return s
}
func main() {
	StudentRegister("Jim", 18)
}

   函数 StudentRegister() 内部的s为局部变量,其值通过函数值返回,s本身为一个指针,其指向的内存地址不会是栈,而是堆,这就是典型的逃逸案例。
   通过编译参数 -gcflags=-m 可以查看编译过程中的逃逸分析过程:

PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:8:6: can inline StudentRegister
./main.go:14:6: can inline main
./main.go:15:17: inlining call to StudentRegister
./main.go:8:22: leaking param: name
./main.go:9:10: new(Student) escapes to heap
./main.go:15:17: new(Student) does not escape
PS D:\Go\workspace\src\lekou> 

  在 StudentRegister() 函数中,代码第9行显示 “escapes to heap” ,表示该行内存分配发生了逃逸现象。

2)栈空间不足逃逸

  当栈空间不足以存放当前对象或无法判断当前切片长度时会将对象分配到堆中。示例代码:

package main

func Slice() {
	s1 := make([]int, 1000, 1000)
	s2 := make([]int, 10000, 10000)

	for index, _ := range s1 {
		s1[index] = index
	}
	for index, _ := range s2 {
		s2[index] = index
	}
}
func main() {
	Slice()
}

  s1的长度为1000,s2的长度为10000。查看编译提示,如下:

PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:3:6: can inline Slice
./main.go:14:6: can inline main
./main.go:15:7: inlining call to Slice
./main.go:4:12: make([]int, 1000, 1000) does not escape		// s1
./main.go:5:12: make([]int, 10000, 10000) escapes to heap	// s2
./main.go:15:7: make([]int, 1000, 1000) does not escape
./main.go:15:7: make([]int, 10000, 10000) escapes to heap

  可以发现s1没有发生逃逸,s2发生逃逸。

3)动态类型逃逸

  很多函数的参数为 interface 类型,比如 fmt.Println(a …interface{}),编译期间很难确定其参数的具体类型,也会产生逃逸,如以下代码所示。

package main

import "fmt"

func main() {
	s := "Hello World!"
	fmt.Println(s)
}

  上述代码中的 s 变量只是一个 string类型变量,调用 fmt.Println()时会产生逃逸:

PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:7:13: inlining call to fmt.Println
./main.go:7:13: ... argument does not escape
./main.go:7:14: s escapes to heap

4)闭包引用对象逃逸

  某著名的开源框架实现了某个返回Fibonacci数列的函数:

func Fibonacci() func() int {
	a, b := 0, 1
	return func() int {
		a, b = b, a+b
		return a
	}
}

  该函数返回一个闭包,闭包引用了函数的局部变量 a 和 b,使用时通过该函数获取闭包,然后每次执行闭包都会依次输出 Fibonacci 数列。完整的示例程序如下:

package main

import "fmt"

func Fibonacci() func() int {
	a, b := 0, 1
	return func() int {
		a, b = b, a+b
		return a
	}
}

func main() {
	f := Fibonacci()

	for i := 0; i < 10; i++ {
		fmt.Printf("Fibonacci:%d\n", f())
	}
}

  上述代码通过 Fibonacci()获取一个闭包,每次执行闭包就会打印一个 Fibonacci 数值。输出如下:

C:\Users\666\AppData\Local\Temp\GoLand\___293go_build_lekou_.exe
Fibonacci:1
Fibonacci:1
Fibonacci:2
Fibonacci:3
Fibonacci:5
Fibonacci:8
Fibonacci:13
Fibonacci:21
Fibonacci:34
Fibonacci:55

Process finished with the exit code 0

  Fibonacci() 函数中原本属于局部变量的 a 和 b 由于闭包的引用,不得不将二者放到堆中,以致产生逃逸:

PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:3:6: can inline Slice
./main.go:10:6: can inline main
./main.go:11:7: inlining call to Slice
./main.go:4:11: make([]int, 1000, 1000) does not escape
./main.go:14:16: inlining call to Fibonacci
./main.go:7:9: can inline main.Fibonacci.func1
./main.go:17:35: inlining call to main.Fibonacci.func1
./main.go:17:13: inlining call to fmt.Printf
./main.go:6:2: moved to heap: a
./main.go:6:5: moved to heap: b
./main.go:7:9: func literal escapes to heap
./main.go:14:16: func literal does not escape
./main.go:17:13: ... argument does not escape
./main.go:17:35: ~r0 escapes to heap

3. 小结

  • 栈上分配内存比在堆中分配内存有更高的效率
  • 栈上分配内存不需要GC处理
  • 对上分配的内存使用完毕会交给GC处理
  • 逃逸分析的目的是决定分配地址是栈还是堆
  • 逃逸分析在编译阶段完成

4. 问题

  思考一下这个问题:函数传递指针真的比传值的效率高吗?

  我们知道传递指针可以减少底层值的复制,可以提高效率,但是如果复制的数据量小,由于指针传递会产生逃逸,则可能会使用堆,也可能增加GC的负担,所以传递指针不一定是高效的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值