Golang逃逸分析浅谈

众所周知,Golang是一门自带GC的编程语言。这意味着内存的分配和管理绝大多数情况下不需要开发者去过多干涉。

在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。
当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或是返回到调用者子程序。如果使用尾递归优化(通常在函数编程语言中是需要的),对象也可以看作逃逸到被调用的子程序中。如果一种语言支持第一类型的延续性在Scheme和Standard ML of New Jersey中同样如此),部分调用栈也可能发生逃逸。
如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中被访问到的地方无法确定——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,因为全局变量是可以在当前子程序之外访问的,此时指针也发生了逃逸。
逃逸分析确定某个指针可以存储的所有地方,以及确定能否保证指针的生命周期只在当前进程或线程中。

引用自 维基百科

简单梳理一下,逃逸分析的讨论范围,主要是值的可达性,保证在需要的时候能够获取到相对应的值。在Golang的具体语境下,逃逸分析一般是讨论编译时决定值是存放在栈上,还是逃逸到堆上
首先,明确一下堆栈的定义:

  • 栈 栈是为每个具体函数分配的内存片段,栈的生存周期等同于函数的生命周期,函数执行完栈空间会被回收
  • 堆 堆是运行时动态使用的内存块,堆的生存周期为整个程序的生命周期,甚至更长

那么编译器究竟如何决定值的分配区域呢?简单一句话就是:需要在栈帧之间传递的,分配在堆上;大对象分配在堆上;尽在栈内部使用的,分配在栈上。
下面通过几个具体示例来简单说明,我们可以通过go的gcflags来把逃逸部分的信息打印出来:

go build -gcflags "-m -l" demo.go
示例1
package main

type People struct {
    Name string
}

//go:noinline
func newPeople1() People {
    return People{
        Name: "test1",
    }
}

//go:noinline
func newPeople2() *People {
    return &People{
        Name: "test2",
    }
}

func main() {
    p1 := newPeople1()
    p2 := newPeople2()
    _, _ = p1, p2
}

运行结果:

# command-line-arguments
./demo.go:16:9: &People literal escapes to heap
# 这里运行结果可以看出,只有newPeople2函数的返回值才逃逸到堆上,这是因为newPeople2函数返回的是指针,为了确保这个指针在newPeople2方法执行完之后依旧可以访问,只能将它分配到堆上
示例2
...
// 其他部分代码如*示例1*
func main() {
    p1 := newPeople1()
    p2 := newPeople2()
    fmt.Println(p1, p2)
}

执行结果:

# command-line-arguments
./demo.go:18:9: &People literal escapes to heap
# 这里同示例1的解释
./demo.go:26:13: ... argument does not escape
./demo.go:26:13: p1 escapes to heap
# 因为在Println函数中需要p1的值,所以p1也逃逸到了堆上
示例3
...
// 其他部分代码如*示例1*
//go:noinline
func GetPeople1(p *People) *People {
    p1 := p
    return p1
}

//go:noinline
func GetPeople2(p *People) *People {
    p1 := *p
    return &p1
}

func main() {
    p := People{Name: "test3"}
    _ = GetPeople1(&p)
    _ = GetPeople2(&p)
}

运行结果:

# command-line-arguments
./demo.go:16:9: &People literal escapes to heap
./demo.go:22:17: leaking param: p to result ~r1 level=0
./demo.go:28:17: leaking param content: p
./demo.go:29:2: moved to heap: p1
# 因为GetPeople2函数中返回值是栈内数据(p1)的地址(&p1),为了保证这个值能够被安全访问到,所以p1逃逸到堆上。
示例4
package main

type People struct {
    Name *string
    Age  int32
}

func setPeople1(p People, name string) People {
    p.Name = &name
    return p
}

func setPeople2(p People, age int32) People {
    p.Age = age
    return p
}

func main() {
    p := People{}
    _ = setPeople1(p, "Mike")
    _ = setPeople2(p, 25)
}

运行结果:

# command-line-arguments
./demo.go:8:17: leaking param: p to result ~r2 level=0
./demo.go:8:27: moved to heap: name
# 在setPeople1函数中,返回值的Name字段的值指向一个栈内的值(name)的地址,所以逃逸到堆上
./demo.go:13:17: leaking param: p to result ~r2 level=0
示例5
package main

func sliceAppend(s []int64) []int64 {
    for i := 0; i < 1000; i++ {
        s = append(s, int64(i))
    }
    return s
}

func sliceSlice(s []int64) []int64 {
    return s[1:]
}

func mapExtend(m map[int64]int64) map[int64]int64 {
    for i := 0; i < 1000; i++ {
        m[int64(i)] = int64(2 * i)
    }
    return m
}

func mapRemove(m map[int64]int64) map[int64]int64 {
    for k, v := range m {
        if k%2 == 0 || v%2 == 0 {
            delete(m, k)
        }
    }
    return m
}

func main() {
    s := make([]int64, 32)
    m := make(map[int64]int64)
    s = sliceAppend(s)
    s = sliceSlice(s)
    m = mapExtend(m)
    m = mapRemove(m)
}

运行结果:

# command-line-arguments
./demo.go:3:18: leaking param: s to result ~r1 level=0
./demo.go:10:17: leaking param: s to result ~r1 level=0
./demo.go:14:16: leaking param: m to result ~r1 level=0
./demo.go:21:16: leaking param: m to result ~r1 level=0
./demo.go:31:11: make([]int64, 32) does not escape
./demo.go:32:11: make(map[int64]int64) does not escape
# 从结果来看,slice和map都是简单的值传递,不涉及任何逃逸行为

总结

逃逸分析对于我们实际编程有什么意义呢?我们上面提到过,分配在栈上的空间会随着函数执行完而被回收,所以我们日常提到的GC主要是指各种对于堆上内存空间的追踪和清理。理解逃逸分析有助于我们写出GC友好的代码(通过减少堆上内存分配来减少内存压力),从另一个角度来看,如果代码不分配在堆上,就意味着有可能会在多个栈中存在多个拷贝,额外空间占用和GC开销之间需要作出权衡。

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