Go 逃逸分析

Go 逃逸分析

堆和栈

要理解什么是逃逸分析会涉及堆和栈的一些基本知识,如果忘记的同学我们可以简单的回顾一下:

  • 堆(Heap):一般来讲是人为手动进行管理,手动申请、分配、释放。堆适合不可预知大小的内存分配,这也意味着为此付出的代价是分配速度较慢,而且会形成内存碎片。
  • 栈(Stack):由编译器进行管理,自动申请、分配、释放。一般不会太大,因此栈的分配和回收速度非常快;我们常见的函数参数(不同平台允许存放的数量不同),局部变量等都会存放在栈上。

栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。

通俗比喻的说,就如我们去饭馆吃饭,只需要点菜(发出申请)--》吃吃吃(使用内存)--》吃饱就跑剩下的交给饭馆(操作系统自动回收),而就如在家里做饭,大到家,小到买什么菜,每一个环节都需要自己来实现,但是自由度会大很多。

什么是逃逸分析

在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法,简单来说就是分析在程序的哪些地方可以访问到该指针。

再往简单的说,Go是通过在编译器里做逃逸分析(escape analysis)来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上;即我发现变量在退出函数后没有用了,那么就把丢到栈上,毕竟栈上的内存分配和回收比堆上快很多;反之,函数内的普通变量经过逃逸分析后,发现在函数退出后变量还有在其他地方上引用,那就将变量分配在堆上。做到按需分配(哪里的人民需要我,我就往哪去~~,一个党员的呐喊)。

为何需要逃逸分析

ok,了解完各自的优缺点后,我们就可以更好的知道逃逸分析存在的目的了:

  1. 减少gc压力,栈上的变量,随着函数退出后系统直接回收,不需要gc标记后再清除。
  2. 减少内存碎片的产生。
  3. 减轻分配堆内存的开销,提高程序的运行速度。

如何确定是否逃逸

Go中通过逃逸分析日志来确定变量是否逃逸,开启逃逸分析日志:

go run -gcflags ‘-m -l‘ main.go
  • -m 会打印出逃逸分析的优化策略,实际上最多总共可以用 4 个 -m,但是信息量较大,一般用 1 个就可以了。
  • -l 会禁用函数内联,在这里禁用掉内联能更好的观察逃逸情况,减少干扰。

逃逸案例

案例一:取地址发生逃逸

package main?type UserData struct {  Name  string}?func main() {  var info UserData  info.Name = "WilburXu"  _ = GetUserInfo(info)}?func GetUserInfo(userInfo UserData) *UserData {  return &userInfo}

执行 go run -gcflags ‘-m -l‘ main.go 后返回以下结果:

# command-line-arguments.\main.go:14:9: &userInfo escapes to heap.\main.go:13:18: moved to heap: userInfo

GetUserInfo函数里面的变量 userInfo 逃到堆上了(分配到堆内存空间上了)。

GetUserInfo 函数的返回值为 *UserData 指针类型,然后 将值变量userInfo 的地址返回,此时编译器会判断该值可能会在函数外使用,就将其分配到了堆上,所以变量userInfo就逃逸了。

优化方案

func main() {  var info UserData  info.Name = "WilburXu"  _ = GetUserInfo(&info)}?func GetUserInfo(userInfo *UserData) *UserData {  return userInfo}
# command-line-arguments.\main.go:13:18: leaking param: userInfo to result ~r1 level=0.\main.go:10:18: main &info does not escape

对一个变量取地址,可能会被分配到堆上。但是编译器进行逃逸分析后,如果发现到在函数返回后,此变量不会被引用,那么还是会被分配到栈上。套个取址符,就想骗补助?

编译器傲娇的说:Too young,Too Cool...!

案例二 :未确定类型

package main?type User struct {  name interface{}}?func main() {  name := "WilburXu"  MyPrintln(name)}?func MyPrintln(one interface{}) (n int, err error) {  var userInfo = new(User)  userInfo.name = one // 泛型赋值 逃逸咯  return}

执行 go run -gcflags ‘-m -l‘ main.go 后返回以下结果:

# command-line-arguments./main.go:12:16: leaking param: one./main.go:13:20: MyPrintln new(User) does not escape./main.go:9:11: name escapes to heap

这里可能有同学会好奇,MyPrintln函数内并没有被引用的便利,为什么变了name会被分配到了上呢?

上一个案例我们知道了,普通的手法想去"骗取补助",聪明灵利的编译器是不会“上当受骗的噢”;但是对于interface类型,很遗憾,编译器在编译的时候很难知道在函数的调用或者结构体的赋值过程会是怎么类型,因此只能分配到上。

优化方案

将结构体User的成员name的类型、函数MyPringLn参数one的类型改为 string,将得出:

# command-line-arguments./main.go:12:16: leaking param: one./main.go:13:20: MyPrintln new(User) does not escape

拓展分析

对于案例二的分析,我们还可以通过反编译命令go tool compile -S main.go查看,会发现如果为interface类型,main主函数在编译后会额外多出以下指令:

# main.go:9 -> MyPrintln(name)  0x001d 00029 (main.go:9)  PCDATA  $2, $1  0x001d 00029 (main.go:9)  PCDATA  $0, $1  0x001d 00029 (main.go:9)  LEAQ  go.string."WilburXu"(SB), AX  0x0024 00036 (main.go:9)  PCDATA  $2, $0  0x0024 00036 (main.go:9)  MOVQ  AX, ""..autotmp_5+32(SP)  0x0029 00041 (main.go:9)  MOVQ  $8, ""..autotmp_5+40(SP)  0x0032 00050 (main.go:9)  PCDATA  $2, $1  0x0032 00050 (main.go:9)  LEAQ  type.string(SB), AX  0x0039 00057 (main.go:9)  PCDATA  $2, $0  0x0039 00057 (main.go:9)  MOVQ  AX, (SP)  0x003d 00061 (main.go:9)  PCDATA  $2, $1  0x003d 00061 (main.go:9)  LEAQ  ""..autotmp_5+32(SP), AX  0x0042 00066 (main.go:9)  PCDATA  $2, $0  0x0042 00066 (main.go:9)  MOVQ  AX, 8(SP)  0x0047 00071 (main.go:9)  CALL  runtime.convT2Estring(SB)

对于Go汇编语法不熟悉的可以参考 Golang汇编快速指南

总结

不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。

Go的编译器就如一个聪明的孩子一般,大多时候在逃逸分析问题上的处理都令人眼前一亮,但有时闹性子的时候处理也是非常粗糙的分析或完全放弃,毕竟这是孩子天性不是吗? 所以也需要我们在编写代码的时候多多观察,多多留意了。

参考文章

Golang escape analysis

原文地址:https://www.cnblogs.com/wilburxu/p/11184604.html

时间: 2024-07-31 19:34:30

Go 逃逸分析的相关文章

逃逸分析

[分析对象动态作用域] 方法逃逸,线程逃逸. ——栈上分配:对象可以随着方法的结束而自动销毁. ——同步消除 ——标量替换:将对象中使用到的成员变量恢复原始类型来使用. ======================================================================= 在编程语言的编译优化原理中,分析指针动态范围的方法称之为逃逸分析.它跟静态代码分析技术中的指针分析和外形分析类似. 通俗一点讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生

[转]Java内存对象的逃逸分析

逃逸分析英文作Escape Analysis.在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联. 当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape). 在Java 并发编程书里有个例子程序清单3-7 谈到 this escape. 开始没有想明白, 仔细琢磨了些时间发现代码主要的问题是在建构函数中创建了一个匿名类,然后发布了这个匿名类

十、逃逸分析和栈上分配

Java堆区已经不再是对象实例分配的唯一空间,可以在堆区之外分配内存以提升效率降低频率,逃逸分析即是如此. 什么是逃逸分析? 例如: 一个成员方法的内部实例化了一个对象,如果这个对象被方法外的引用指向了,那么就发生了逃逸现象.JVM在内存分配的时候会分析其是否发生逃逸,如果未发生逃逸的,那么就直接在栈上分配内存空间,其生命周期和线程相同.(也称之为"栈上分配") 原文地址:https://www.cnblogs.com/lay2017/p/8157760.html

JVM逃逸分析DoEscapeAnalysis

JVM逃逸分析 JVM有栈.堆.方法区.本地栈等组成 栈:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表.操作栈.动态连接.方法出口等信息.每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程. 堆:当实例化对象时,会把对象分配到堆中,然后把指向该堆的引用压入栈中. 逃逸:当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸,一般情况返回对象.对全局变量的肤质一般都会发生逃逸. 逃逸分析:用来分析这种逃逸现象的方法称为逃逸分析 逃逸分析优化-栈

深入理解Java中的逃逸分析

在Java的编译体系中,一个Java的源代码文件变成计算机可执行的机器指令的过程中,需要经过两段编译,第一段是把.java文件转换成.class文件.第二段编译是把.class转换成机器指令的过程. 第一段编译就是javac命令. 在第二编译阶段,JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译.很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢很多.这就是传统的JVM的解释器(Interpreter)的功能.为了解决这种效率问题,引入了 JIT(即时编译)

逃逸分析(Escape Analysis)

什么是逃逸? 逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到:这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用.正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象:故由于无法回收,即成为逃逸. /** * 无逃逸 */ void test01() { String test1 = "test1"; } String test2; /** * 逃逸 */ void test

java虚拟机的逃逸分析

逃逸分析作为其他优化手段提供依据的分析技术,其基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸.甚至还有可能被外部线程访问到,比如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸. 如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或者线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高校的优化. 1)栈上分配,如果确定一个对象不会逃逸出方法之外,那么把这个对象在栈上分配内存,对象所占用

关于JVM的逃逸分析

何谓“逃逸”? 我们都知道Java中的对象默认是分配到堆上的,垃圾回收机制也会回收堆中不再使用的对象,但在此之前需要筛选可回收的对象,因此会造成,回收对象还有整理内存,都比较耗时间,开销也是非常之大.而此也是Java语言被疯狂吐槽的一地方,就是Java不支持栈上分配对象.而在我们日常开发中,内存,时间都是相当的宝贵,如何优化成为在开发中一个不可或缺的环节. 逃逸分析(Escape Analysis),是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法.通过逃逸分

JVM逃逸分析

JDK1.8默认开启逃逸分析,这是一种代码分析手段,能动态分析对象的作用域,为其它优化手段如栈上分配.标量替换和同步消除等提供依据. 一共可能有两种逃逸行为:方法逃逸和线程逃逸. 方法逃逸:当一个对象在方法中定义之后,作为参数传递到其它方法中: 线程逃逸:如类变量或实例变量,可能被其它线程访问到: 如果确认不存在逃逸行为,则可以对该对象进行如下优化:同步消除.标量替换和栈上分配. 参考:https://www.jianshu.com/p/20bd2e9b1f03 原文地址:https://www

【Block-Level Verification】 芯片开发通识_验证目标_ 验证语言_ 验证职业前景 _挑战和瓶颈_验证周期_功能描述文档_验证计划_回归测试_硅后测试_逃逸分析

SystemVerilog验证通识 1. 芯片开发概述 不同于通用电路,专用集成电路为了专门解决或者优化相关工程问题,例如专用算法的电路实现,如芯片里加入人工智能处理单元,为CPU\GPU减负,目的是提高应用效率和降低能耗. 芯片体积有多大?2017年5月 一款芯片采用12nm FFN 工艺,核心面积为惊人的815平方mm,一共包含211亿个晶体管.大于10亿门为大型SOC,现在非常多,一款4G 芯片大约为40-50亿门. 28nm流片价格为 200万美金,14nm double,7nm dou