今天偶然看到Golang关于内存的文章,其中涉及了一点逃逸分析,由于去年之前都是专研C++,Golang也是去年11月才开始学习的,学完就马上进入项目了,没有深究底层,准备这段时间边改论文边开始仔细学一下Golang。

测试环境:

首先是逃逸分析的介绍

C/C++和Golang的变量内存分配方式不一样,前者是程序员来决定,比如可以使用malloc/new来将对象存在堆上,而Golang是编译器根据变量决定内存分配位置的方式,分配的方法叫做逃逸分析(escape analysis),这个阶段在编译时期完成,与JVM不一样,JVM是运行时期进行逃逸分析。

C++例子

其次在看一个C++下的内存空间的例子,C++不允许函数返回在函数体内初始化变量的指针:

#include <stdio.h>int *escape_test(int arg_val) {int foo_val = 11;return &foo_val;
}int main()
{int *main_val = escape_test(666);printf("%d\n", *main_val);
}

编译发生warning【返回了局部变量的地址】,随即运行,发生了core dump

本地没有core文件,忘记打开了core限制,使用命令:

ulimit -c unlimited  //开启 core dump 功能

然后重新编译并开启gdb调试可以看到错误现场:

gdb ./app core

错误原因和warning原因一致:是由于函数返回了局部变量的地址。学过C++的都知道普通函数局部变量是在栈上实现内存分配的,随着普通函数的结束,栈上的变量将会被自动释放,如果这时候返回变量的地址将导致不确定的结果。

Golang逃逸

而将上述代码使用Golang的语法进行翻译:

package mainfunc escape_test(arg_val int)(*int) {var foo_val int = 11;return &foo_val;
}func main() {main_val := escape_test(45)println(*main_val)
}

运行顺利,并得到输出:

ubuntu@VM-4-8-ubuntu:~/go/src/go_code/escape_01$ go run main.go
11

这就是Go的逃逸分析所做的事情,通过编译器来决定对将变量分配在哪,当发现变量的作用域没有跑出函数范围,则可以分配在栈上,反之则必须分配在堆。

例子:

package main// golang逃逸
func escape_test(arg int)( *int){var param_1 int =1;var param_2 int =2;var param_3 int =3;var param_4 int =4;var param_5 int =5;for i := 0; i < 5; i++ {println(&arg,&param_1,&param_2,&param_3,&param_4,&param_5)   }return &param_3
}func main() {main_val := escape_test(666)println(*main_val, main_val)
}

这个example是在一个博客上看到的,然后自己重新写的时候遇到了两个坑,主要是编译器优化导致的等下可能会细说这两个坑。运行上述代码,会得到结果:

可以看出param_3和其他变量不是连续分配在一块的,而其他四个变量是存储在一起的,使用go内嵌的编译命令可以发现,param_3逃逸到堆上了。

ubuntu@VM-4-8-ubuntu:~/go/src/go_code/escape_01$ go tool compile -m main.go
main.go:18:6: can inline main
main.go:15:9: &param_3 escapes to heap
main.go:8:6: moved to heap: param_3
main.go:13:11: escape_test &arg does not escape
main.go:13:16: escape_test &param_1 does not escape
main.go:13:25: escape_test &param_2 does not escape
main.go:13:34: escape_test &param_3 does not escape
main.go:13:43: escape_test &param_4 does not escape
main.go:13:52: escape_test &param_5 does not escape

通过tool compile工具得到汇编语句,可以看出,param_3变量是被分配在堆上的:

接下来,将五个局部变量都通过new的方式进行创建:

package main// golang逃逸
func escape_test(arg int)( *int){var param_1 *int =new(int);var param_2 *int =new(int);var param_3 *int =new(int);var param_4 *int =new(int);var param_5 *int =new(int);for i := 0; i < 5; i++ {println(&arg,param_1,param_2,param_3,param_4,param_5)    }return param_3
}func main() {main_val := escape_test(666)println(*main_val, main_val)
}

运行结果:

ubuntu@VM-4-8-ubuntu:~/go/src/go_code/escape_01$ go run main.go
0xc00003a768 0xc00003a738 0xc00003a730 0xc000018070 0xc00003a748 0xc00003a740
0xc00003a768 0xc00003a738 0xc00003a730 0xc000018070 0xc00003a748 0xc00003a740
0xc00003a768 0xc00003a738 0xc00003a730 0xc000018070 0xc00003a748 0xc00003a740
0xc00003a768 0xc00003a738 0xc00003a730 0xc000018070 0xc00003a748 0xc00003a740
0xc00003a768 0xc00003a738 0xc00003a730 0xc000018070 0xc00003a748 0xc00003a740
0 0xc000018070

可以发现param1、 param2、param4、param5与param3的地址依旧不连续,尽管都分配在堆上,但还是发生了逃逸。

逃逸场景

  1. 指针逃逸:如果一个函数内部创建了一个对象(局部变量),但是在函数返回时是返回该对象的指针,那么该变量的生命周期就变了,即使当前函数执行结束了,但是变量的指针还在,并不是随着函数结束就被回收的,那么这个局部变量就会被分配在堆上,这就产生了指针逃逸。
  2. 栈空间不足逃逸:栈空间不足以存放当前对象或者无法判断当前切片长度时会将对象分配到堆中
  3. 动态类型逃逸:函数的参数为interface类型,编译期间很难确定参数的具体类型,也会产生逃逸
  4. 变量大小不确定:
  5. 闭包引用对象逃逸:由于闭包的引用,不得不将引用对象其放到堆中,以至于产生逃逸
  6. 引用类成员的引用:一般我们给一个引用类对象中的引用类成员进行赋值,可能出现逃逸现象。可以理解为访问一个引用对象实际上底层就是通过一个指针来间接的访问了,但如果再访问里面的引用成员就会有第二次间接访问,这样操作这部分对象的话,极大可能会出现逃逸的现象。Go语言中的引用类型有func(函数类型),interface(接口类型),slice(切片类型),map(字典类型),channel(管道类型),*(指针类型)等。

第六个场景的例子有:[]interface{}数据类型、map[string]interface{}、map[interface{}]interface{}、map[string][]string、[]*int、func(*int)、chan []string等,对这些数据类型赋值的时候会对传递的值进行逃逸

Q1:传值还是传指针?

传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆中,增加垃圾回收(GC)的负担。在对象频繁创建和删除的场景下,传递指针导致的 GC 开销可能会严重影响性能。

一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能。

坑:

我在测试golang逃逸的时候出现了一个坑,那就是go编译器将函数escape_test优化成inline(内联函数),如果是内联函数,main调用escape_test将是原地展开,所以param1-5相当于main作用域的变量,这就不会发生逃逸

package main// golang逃逸
func escape_test(arg int)( *int){var param_1 int =1;var param_2 int =2;var param_3 int =3;var param_4 int =4;var param_5 int =5;println(&arg,&param_1,&param_2,&param_3,&param_4,&param_5) return &param_3
}func main() {main_val := escape_test(666)println(*main_val, main_val)
}

而导致的结果是:

即加了循环就会防止escape_test成为内联函数,理由:

函数调用是存在一些固定开销的,例如维护帧指针寄存器BP、栈溢出检测等。因此,对于一些代码行比较少的函数,编译器倾向于将它们在编译期展开从而消除函数调用,这种行为就是内联。【其他关于内联的可以看扩展知识】

扩展知识:

1、go tool compile常见操作

go tool compile [flags] file...
-NDisable optimizations.
-SPrint assembly listing to standard output (code only).
-lDisable inlining.
-mPrint optimization decisions. Higher values or repetitionproduce more detail.
-packWrite a package (archive) file rather than an object file
-raceCompile with race detector enabled.
-sWarn about composite literals that can be simplified.

2、

go build -gcflags="-m -m" main.go命令 查看编译器的优化策略。

函数由于存在循环语句并不能被内联:cannot inline iter: unhandled op FOR;实际上,除了for循环,还有一些情况不会被内联,例如闭包,selectfordefergo关键字所开启的新goroutine等。

Go程序编译时,默认将进行内联优化。我们可通过-gcflags="-l"选项全局禁用内联,与一个-l禁用内联相反,如果传递两个或两个以上的-l则会打开内联,并启用更激进的内联策略。如果不想全局范围内禁止优化,则可以在函数定义时添加 //go:noinline 编译指令来阻止编译器内联函数。

记录一次Golang逃逸分析相关推荐

  1. golang逃逸分析

    变量和栈有什么关系 栈可用于内存分配,栈的分配和回收速度非常快.下面代码展示栈在内存分配上的作用,代码如下: func calc(a, b int) int {var c intc = a * bva ...

  2. golang int 转string_Golang的逃逸分析

    逃逸分析 逃逸分析(Escape Analysis)指的是将变量的内存分配在合适的地方(堆或者栈). 在函数中申请内存有2种情况: - 如果内存分配在栈(stack)上,当函数退出的时候,这部分内存会 ...

  3. GoLang的逃逸分析

    GoLang的垃圾回收机制可以进行自动内存管理让我们的代码更简洁,同时发生内存泄漏的可能性更小.然而,GC会定期停止并收集未使用的对象,因此还是会增加程序的开销.Go的编译器十分聪明,比如决定变量需要 ...

  4. Golang 内存分配与逃逸分析

    参考:灵魂拷问:Go 语言这个变量到底分配到哪里了? 来源于 公众号: 脑子进煎鱼了 ,作者陈煎鱼. 我们在写代码的时候,有时候会想这个变量到底分配到哪里了?这时候可能会有人说,在栈上,在堆上.信我准 ...

  5. 通过实例理解 Go 逃逸分析

    本文转载自白明老师,这是中文社区里面最好.最全面的一篇关于逃逸分析的文章,写得非常好.既有理论.又有实践,引经据典,精彩至及. 翻看了一下自己的Go文章归档[1],发现自己从未专门写过有关Go逃逸分析 ...

  6. 通过实例理解Go逃逸分析

    翻看了一下自己的Go文章归档[1],发现自己从未专门写过有关Go逃逸分析(escape analysis)的文章.关于Go变量的逃逸分析,大多数Gopher其实并不用关心,甚至可以无视.但是如果你将G ...

  7. 3704对象关闭时_JVM 通过逃逸分析就能让对象在栈上分配?没那么简单!

    本文转载自公众号 星哥笔记 作者:Danny姜 校对:承香墨影 经常会有面试官会问一个问题:Java 中的对象都是在"堆"中创建吗? 然后跟求职者大谈特谈「逃逸分析」,说通过「逃逸 ...

  8. 锁优化:逃逸分析、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁

    1. 逃逸分析 Escape Analysis 1.1 逃逸分为两种: 方法逃逸:当一个对象在方法中被定义后,可能作为调用参数被外部方法说引用. 线程逃逸:通过复制给类变量或者作为实例变量在其他线程中 ...

  9. 3.内存分配、逃逸分析与栈上分配、直接内存和运行时常量池、基本类型的包装类和常量池、TLAB、可达性分析算法(学习笔记)

    3.JVM内存分配 3.1.内存分配概述 3.2.内存分配–Eden区域 3.3.内存分配–大对象直接进老年代 3.3.1.背景 3.3.2.解析 3.4.内存分配–长期存活的对象进去老年代 3.5. ...

最新文章

  1. linux目录结构   各个目录文件作用
  2. 痛与快乐有一个代码是什么_养一只真大型犬的生活是什么样的?铲屎官:痛并快乐着!...
  3. Vim使用技巧:撤销与恢复撤销
  4. tocmat linux搭建测试环境,Apache+Tomcat 环境搭建(JK部署过程)
  5. 20 世纪 70 年代的太空殖民艺术
  6. 2020 ccf推荐中文期刊_中国计算机学会推荐中文期刊目录,让业内学者不再盲目投稿...
  7. Git合并最近的commit
  8. 有PHP4的分支吗?
  9. Kali下的钓鱼工具setoolkit和社工字典工具Cupp
  10. openharmony常用网站
  11. 短视频、移动AI……你关注的热点移动开发技术都在这
  12. 苹果怎么清理隐藏内存?全新手机技巧,还不会的亏大了!
  13. Dell服务器型号的详解
  14. 组建linux虚拟团队,虚拟团队建设与管理
  15. 什么是TDK?什么是网站的TDK?扫(myself的)盲
  16. 题目:利用指针知识,写一函数,求一个字符串的长度
  17. 2884: 水果分级
  18. 高等学校计算机水平考试分值,请问计算机二级ps的考试题型和分值是怎样的?...
  19. Echart.js China.js制作中国热力图
  20. 艾永亮:国产运动品牌巨头一一倒下,被抛弃的品牌该如何自救?

热门文章

  1. mysql碎片整理innodb_Innodb表碎片整理
  2. 相机上的这个自定义功能太好用了
  3. 数据中台各种架构图大全
  4. 一个生成公章图片的简易工具
  5. axios 全攻略之 API
  6. 刮刮彩票 (20 分)
  7. 程序员的工资这么高,为什么还会有人离职?
  8. 海王算法(看完不会变成海王)
  9. linux ftp去不了文件损坏,Linux下使用ftp上传压缩文件,windows下载打开损坏问题
  10. SSM毕设项目某企业危化品信息管理系统bf339(java+VUE+Mybatis+Maven+Mysql)