go逃逸场景有哪些???

简介: 逃逸分析就是程序运行时内存的分配位置(栈或堆),是由编译器来确定的,而非开发者。

逃逸分析就是程序运行时内存的分配位置(栈或堆),是由编译器来确定的,而非开发者。

什么是栈

栈只允许从线性表的同一端放入和取出数据,按照后进先出(LIFO,Last InFirst Out)的顺序,如下图:

image.png

什么是堆

对于堆在内存中的分配,我们可以类比成一个房间,分配内存时,需要找一块足够装下家具的空间来摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往这个空间里摆放家具会发现虽然有足够的空间,但各个空间分布在不同的区域,没有一段连续的空间来摆放家具。此时,内存分配器就需要对这些空间进行调整优化,如下图:

image.png

对比栈和堆可知,在编译时,一切无法确定大小或大小可以改变的数据,最好放到堆上,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

函数中申请一个新的对象:

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

逃逸分析的好处应该是减少了 gc 的压力,栈的分配比堆快,性能好,如果变量都分配到栈上,可以避免 Go 频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销。

逃逸分析基本原则

编译器会根据变量是否被外部引用来决定是否逃逸:

  1. 如果函数外部没有引用,则优先放到栈中;
  2. 如果函数外部存在引用,则必定放到堆中;
  3. 如果栈上放不开,则必定放到堆上;

逃逸场景

指针逃逸

我们知道Go可以返回局部变量指针,这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。

package main
type Person struct {
  Name string
  Age  int
}
func PersonRegister(name string, age int) *Person {
  p := new(Person) //局部变量s逃逸到堆
  p.Name = name
  p.Age = age
  return p
}
func main() {
  PersonRegister("微客鸟窝", 18)
}
复制代码

函数 PersonRegister() 内部 p 为局部变量,其值通过函数返回值返回, p 本身为一指针,其指向的内存地址不会是栈而是堆,这就是典型的逃逸案例。

通过编译参数-gcflag=-m可以查看编译过程中的逃逸分析:

$ go build -gcflags=-m
# ceshi
.\main.go:8:6: can inline PersonRegister
.\main.go:17:6: can inline main
.\main.go:18:16: inlining call to PersonRegister
.\main.go:8:21: leaking param: name
.\main.go:9:10: new(Person) escapes to heap
.\main.go:18:16: new(Person) does not escape
复制代码

代码第9行显示”escapes to heap”,表示该行内存分配发生了逃逸现象。

栈空间不足逃逸

package main
func Slice() {
  s := make([]int, 1000, 1000)
  for index, _ := range s {
    s[index] = index
  }
}
func main() {
  Slice()
}
复制代码

上面代码 Slice() 函数中分配了一个1000个长度的切片,是否逃逸取决于栈空间是否足够大。直接查看编译提示,如下:

$ go build -gcflags=-m
# ceshi
.\main.go:11:6: can inline main
.\main.go:4:11: make([]int, 1000, 1000) does not escape
复制代码

发现并没有发生逃逸。我们把切片长度扩大10倍再试试:

s := make([]int, 10000, 10000)

$ go build -gcflags=-m
# ceshi
.\main.go:11:6: can inline main
.\main.go:4:11: make([]int, 10000, 10000) escapes to heap
复制代码

发现当切片长度扩大到10000时就会逃逸。 当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。

动态类型逃逸

在 Go 中,空接口 interface{} 可以表示任意的类型,如果函数参数为 interface{},编译期间很难确定其参数的具体类型,也会发生逃逸。

package main
import "fmt"
func main() {
  s := "wekenw"
  fmt.Println(s)
}
复制代码

因为 fmt.Println() 的参数类型定义为 interface{},因此也发生了逃逸。

$ go build -gcflags=-m
# ceshi
.\main.go:6:13: inlining call to fmt.Println
.\main.go:6:13: s escapes to heap
.\main.go:6:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape
<autogenerated>:1: .this does not escape
复制代码

闭包引用对象逃逸

回Fibonacci数列的函数:

package main
func main() {
  f := fibonacci()
  for i := 0; i < 10; i++ {
    f()
  }
}
func fibonacci() func() int {
  a, b := 0, 1
  return func() int {
    a, b = b, a+b
    return a
  }
}
复制代码

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

$ go build -gcflags=-m
# ceshi
.\main.go:11:9: can inline fibonacci.func1
.\main.go:10:2: moved to heap: a
.\main.go:10:5: moved to heap: b
.\main.go:11:9: func literal escapes to heap
复制代码

总结

  • 栈上分配内存比在堆中分配内存效率更高
  • 栈上分配的内存不需要 GC 处理,而堆需要
  • 逃逸分析目的是决定内分配地址是栈还是堆
  • 逃逸分析在编译阶段完成

传值 VS 传指针

函数传递指针真的比传值效率高吗?如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,增加垃圾回收(GC)的负担,所以传递指针不一定 是高效的。



image.png

相关文章
|
9月前
|
Cloud Native 程序员 编译器
你知道 GO 中什么情况会变量逃逸吗?
你知道 GO 中什么情况会变量逃逸吗?
|
10月前
|
存储 编译器 Go
Go 语言内存逃逸案例
Go 语言内存逃逸案例
46 0
|
10月前
|
Go 索引
案例实战:Go语言for range遍历经典应用场景
案例实战:Go语言for range遍历经典应用场景
77 0
|
10月前
|
Java 编译器 Go
Go 语言逃逸分析
Go 语言逃逸分析
38 0
|
11天前
|
并行计算 数据挖掘 大数据
[go 面试] 并行与并发的区别及应用场景解析
[go 面试] 并行与并发的区别及应用场景解析
|
2月前
|
安全 测试技术 Go
Go语言在高并发场景下的应用
在当今互联网高速发展的时代,高并发已成为众多应用系统面临的核心问题。本文探讨了Go语言在高并发场景下的优势,并通过具体实例展示了其在实际应用中的效果和性能表现。
|
11天前
|
存储 编译器 Go
Go语言中的逃逸分析
Go语言中的逃逸分析
|
16天前
|
设计模式 Java 编译器
Go - 基于逃逸分析来提升程序性能
Go - 基于逃逸分析来提升程序性能
22 2
|
3月前
|
Rust 安全 程序员
Rust vs Go:解析两者的独特特性和适用场景
在讨论 Rust 与 Go 两种编程语言哪种更优秀时,我们将探讨它们在性能、简易性、安全性、功能、规模和并发处理等方面的比较。同时,我们看看它们有什么共同点和根本的差异。现在就来看看这个友好而公平的对比。
|
3月前
|
存储 缓存 Java
先聊聊「内存分配」,再聊聊Go的「逃逸分析」。
今天和大家聊聊Go语言的「内存分配」和「逃逸分析」。