Go 语言较之 C 语言一个很大的优势就是自带 GC 功能,可 GC 并不是没有代价的.写 C 语言的时候,在一个函数内声明的变量,在函数退出后会自动释放掉,因为这些变量分配在栈上.如果你期望变量的数据可以在函数退出后仍然能被访问,就需要调用 malloc 方法在堆上申请内存,如果程序不再需要这块内存了,再调用 free 方法释放掉.Go 语言不需要你主动调用 malloc 来分配堆空间,编译器会自动分析,找出需要 malloc 的变量,使用堆内存.编译器的这个分析过程就叫做逃逸分析.
所以你在一个函数中通过 dict := make(map[string]int) 创建一个 map 变量,其背后的数据是放在栈空间上还是堆空间上,是不一定的.这要看编译器分析的结果.
可逃逸分析并不是百分百准确的,它有缺陷.有的时候你会发现有些变量其实在栈空间上分配完全没问题的,但编译后程序还是把这些数据放在了堆上.如果你了解 Go 语言编译器逃逸分析的机制,在写代码的时候就可以有意识地绕开这些缺陷,使你的程序更高效.
Go 语言虽然在内存管理方面降低了编程门槛,即使你不了解堆栈也能正常开发,但如果你要在性能上较真的话,还是要掌握这些基础知识.
这里举一个小例子,来对比下堆栈的差别:
stack 函数中的变量 i 在函数退出会自动释放;而 heap 函数返回的是对变量 i 的引用,也就是说 heap() 退出后,表示变量 i 还要能被访问,它会自动被分配到堆空间上.
他们编译出来的代码如下:
逻辑的复杂度不言而喻,从上面的汇编中可看到, heap() 函数调用了 runtime.newobject() 方法,它会调用 mallocgc 方法从 mcache 上申请内存,申请的内部逻辑前面文章已经讲述过.堆内存分配不仅分配上逻辑比栈空间分配复杂,它最致命的是会带来很大的管理成本,Go 语言要消耗很多的计算资源对其进行标记回收(也就是 GC 成本).
我们在 go build 编译代码时,可使用 -gcflags '-m' 参数来查看逃逸分析日志.
以上面的两个函数为例,编译的日志输出是:
日志中的 i escapes to heap 表示该变量数据逃逸到了堆上.
需要使用堆空间,所以逃逸,这没什么可争议的.但编译器有时会将 不需要 使用堆空间的变量,也逃逸掉.这里是容易出现性能问题的大坑.网上有很多相关文章,列举了一些导致逃逸情况,其实总结起来就一句话:
多级间接赋值容易导致逃逸 .
这里的多级间接指的是,对某个引用类对象中的引用类成员进行赋值.Go 语言中的引用类数据类型有 func , interface , slice , map , chan , *Type(指针) .
记住公式 Data.Field = Value ,如果 Data , Field 都是引用类的数据类型,则会导致 Value 逃逸.这里的等号 = 不单单只赋值,也表示参数传递.
根据公式,我们假设一个变量 data 是以下几种类型,相应的可以得出结论:
下面给出一些实际的例子:
如果变量值是一个函数,函数的参数又是引用类型,则传递给它的参数都会逃逸.
上例中 te 的类型是 func(*int) ,属于引用类型,参数 *int 也是引用类型,则调用 te(j) 形成了为 te 的参数(成员) *int 赋值的现象,即 te.i = j 会导致逃逸.代码中其他几种调用都没有形成 多级间接赋值 情况.
同理,如果函数的参数类型是 slice , map 或 interface{} 都会导致参数逃逸.
匿名函数的调用也是一样的,它本质上也是一个函数变量.有兴趣的可以自己测试一下.
只要使用了 Interface 类型(不是 interafce{} ),那么赋值给它的变量一定会逃逸.因为 interfaceVariable.Method() 先是间接的定位到它的实际值,再调用实际值的同名方法,执行时实际值作为参数传递给方法.相当于 interfaceVariable.Method.this = realValue
向 channel 中发送数据,本质上就是为 channel 内部的成员赋值,就像给一个 slice 中的某一项赋值一样.所以 chan *Type , chan map[Type]Type , chan []Type , chan interface{} 类型都会导致发送到 channel 中的数据逃逸.
这本来也是情理之中的,发送给 channel 的数据是要与其他函数分享的,为了保证发送过去的指针依然可用,只能使用堆分配.
可变参数如 func(arg ...string) 实际与 func(arg []string) 是一样的,会增加一层访问路径.这也是 fmt.Sprintf 总是会使参数逃逸的原因.
Benchmark 和 pprof 给出的结果:
熟悉堆栈概念可以让我们更容易看透 Go 程序的性能问题,并进行优化.
多级间接赋值会导致 Go 编译器出现不必要的逃逸,在一些情况下可能我们只需要修改一下数据结构就会使性能有大幅提升.这也是很多人不推荐在 Go 中使用指针的原因,因为它会增加一级访问路径,而 map , slice , interface{} 等类型是不可避免要用到的,为了减少不必要的逃逸,只能拿指针开刀了.
大多数情况下,性能优化都会为程序带来一定的复杂度.建议实际项目中还是怎么方便怎么写,功能完成后通过性能分析找到瓶颈所在,再对局部进行优化.
因为如果变量的内存发生逃逸,它的生命周期就是不可知的,其会被分配到堆上,而堆上分配内存不能像栈一样会自动释放,为了解放程序员双手,专注于业务的实现,go实现了gc垃圾回收机制,但gc会影响程序运行性能,所以要尽量减少程序的gc操作.
①.、在方法内把局部变量指针返回,被外部引用,其生命周期大于栈,则溢出.
①.、go语言的接口类型方法调用是动态,所以呢不能在编译阶段确定,所有类型结构转换成接口的过程会涉及到内存逃逸发生,在频次访问较高的函数尽量调用接口.
在C语言中,可以使用malloc和free手动在堆上分配和回收内存.Go语言中,堆内存是通过垃圾回收机制自动管理的,无需开发者指定.那么,Go编译器怎么知道某个变量需要分配在栈上,还是堆上呢?编译器决定内存分配位置的方式,就称之为逃逸分析(escape analysis).逃逸分析由编译器完成,作用于编译阶段.
使用go语言的好处: go语言的设计是务实的, go在针对并发上进行了优化, 并且支持大规模高并发, 又由于单一的码格式, 相比于其他语言更具有可读性, 在垃圾回收上比java和Python更有效, 因为他是和程序同时执行的.
① 进程, 线程, 协程的区别, 协程的优势
①.0. goroutine之间的通信方式
①.1. 测试是怎么做的(单元测试, 压力测试)