Golang逃逸分析

前言

本文翻译自 Alysha Gardner 的一篇博文Golang escape analysis

由于原博客创作时间较早,文中的一些编译显示结果可能存在出入,请参照最新的Go版本编译结果。

正文

垃圾回收是Go语言的一项很方便的功能-自动管理内存让代码变得更干净并且内存泄漏更少。然而,GC同样增加了开销,因为程序需要阶段性地停止并且回收没用的对象。Go编译器足够聪明地自动决定一个变量是否应该分配到需要垃圾回收的堆上,或者是否能够分配到声明该变量的函数的栈结构中。栈变量不像堆变量,栈变量不会带来任何的GC开销,因为它们和剩余的栈数据一起在函数返回时被销毁。

Go的逃逸分析比HotSpot JVM的更简单。最基础的规则是如果一个变量的引用被声明它的函数返回了,那么它就"逃逸"了-它可以在函数返回后被引用,所以它必须分配在堆中。这是复杂的, 通过下面几点体现:

  • 函数调用其他函数
  • 引用被赋值给了结构体成员
  • 切片和映射
  • 使用指向变量的指针的cgo

为了演示逃逸分析,在编译期间,Go构建了一副函数调用的图用于追踪输入参数和返回值的流程。一个函数可能引用它的一个参数,但是如果该引用没有被返回,那么该变量不会逃逸。一个函数同样可能返回一个引用,但是在声明该变量的函数返回前,该引用可能被另一个栈中的函数引用或者没有被返回。为了阐述几个例子,我们使用-gcflags '-m'参数来运行编译器,该参数将会打印冗长的逃逸分析信息:

package main

type S struct{}

func main()  {
    var x S
    _ = identity(x)
}

func identity(x S) S {
    return x
}

你将会使用go run -gcflags '-m -l'编译这个程序--l标识阻止函数identity的内联。该程序将什么也不输出。Go使用值传递语义,所以main中的变量x将总是被拷贝到identity所处的栈中。通常不带引用的代码总是使用栈内存分配,不会有逃逸分析。让我们尝试更难的事情:

package main

type S struct{}

func main()  {
    var x S
    y := &x
    _ = *identity(y)
}

func identity(z *S) *S {
    return z
}

输出为:

./escape.go:11: leaking param: z to result ~r1
./escape.go:7: main &x does not escape

第一行显示了变量的流向: 输入的变量作为输出被返回了。但是identity()没有引用z, 所以变量没有逃逸。在main返回的的经过中,没有x的引用幸存,所以x可以作为main栈结构的的一部分被分配。

第三个试验:

package main

type S struct{}

func main()  {
    var x S 
    _ = *ref(x)
}

func ref(z S) *S {
    return &z
}

输出:

./escape.go:10: moved to heap: z
./escape.go:11: &z escapes to heap

现在有逃逸发生,记住Go是值传递,所以z是来自main中的变量x的一份拷贝,ref返回z的一份引用,所以z不能成为ref栈的一部分-那么当ref返回时该引用指向哪里呢?取而代之,它逃逸到了堆上,即使main在没有重复引用它前立即抛出该引用,Go的逃逸分析也没有熟练到能够识别出这种情况,它仅仅只是查看输入流和返回的变量,在这种情况下值得注意的是ref将会被编译器内联如果我们不停止它。

如果一个引用被赋值给了一个结构体成员会怎样呢?

package main

type S struct{
    M *int
}

func main()  {
    var i int
    refStruct(i)
}

func refStruct(y int) (z S) {
    z.M = &y
    return z
}

输出:

./escape.go:12: moved to heap: y
./escape.go:13: &y escapes to heap

在这种情况下Go仍然可以追踪引用的流程,即使该引用是一个结构体的成员。因为refStruct产生了引用并且返回了它,y必须逃逸,与这种情形比较:

package main

type S struct{
    M *int
}

func main()  {
    var i int 
    refStruct(&i)
}

func refStruct(y *int) (z S) {
    z.M = y
    return z
}

输出:

./escape.go:12: leaking param: y to result z
./escape.go:9: main &i does not escape

因为main发生了引用并且将其传递给了refStruct,该引用不会比声明它的栈存活得更长。这个和先前的程序有略微不同的语义,但是如果第二个程序是足够完整的话,它将会更高效:在第一个例子中i必须分配在main对应的栈中, 然后重新分配在堆中并且作为参数被拷贝到refStruct中。在第二个例子中,i只被分配一次,并且引用被传递。

一个稍微更隐秘一点的例子:

package main

type S struct{
    M *int
}

func main()  {
    var x S
    var i int
    ref(&i, &x)
}

func ref(y *int, z *S)  {
    z.M = y
}

输出:

./escape.go:13: leaking param: y
./escape.go:13: ref z does not escape
./escape.go:9: moved to heap: i
./escape.go:10: &i escapes to heap
./escape.go:10: main &x does not escape

这里的问题是y被赋值给了一个作为输入的结构体。Go不能追踪这种关系-(Go逃逸分析中输入只被允许流向输出)-所以逃逸分析失败并且变量必须被分配在堆中。由于Go逃逸分析的局限性,有很多文档记载的,非常规的情形(Go 1.5中)中变量必须被分配在堆上——参考链接

最后,关于映射(map)和切片如何呢?记住切片和映射实际上是带有指向分配在堆上的内存的指针的Go结构体:切片结构体在reflect包中的SliceHeader, 映射的结构体要稍微难找一点,但是它在hmap, 如果这些结构体没有逃逸那么它们将被分配在栈空间,但是底层数组或者hash桶中的数据每次都会被分配在堆中,避免这种情况的唯一办法是分配一个固定大小的数组(比如[10000]int)。

如果你已经分析了你的程序的堆使用情况并且需要减少GC时间,将频繁分配的变量从堆中移出可能会好一些。这里同样有一个吸引人的主题: 了解更多关于HotSpot JVM如何处理逃逸分析,查阅这片文章, 这篇文章讲解栈分配,并且还涉及辨别何时同步可以被省略。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容