Golang 逃逸分析

Posted jfcat

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang 逃逸分析相关的知识,希望对你有一定的参考价值。

看到一篇文章分析Golang逃逸的总结的不错,翻译如下

Garbage collection is a convenient feature of Go - automatic memory management makes code cleaner and memory leaks less likely. However, GC also adds overhead as the program periodically needs to stop and collect unused objects. The Go compiler is smart enough to automatically decide whether a variable should be allocated on the heap, where it will later need to be garbage collected, or whether it can be allocated as part of the stack frame of the function which declared it. Stack-allocated variables, unlike heap-allocated variables, don’t incur any GC overhead because they’re destroyed when the rest of the stack frame is destroyed - when the function returns

垃圾收集对Go语言来说是一个便利的特色----自动内存管理使代码更清晰同时更少的内存泄漏可能。然后GC同时添加的程序周期性收集不再使用对象的需要。Go compiler 足够只能的自动分析决定变量是否应该分配到堆上,并在堆上被GC回收,或者它被分配作为声明变量的函数栈帧的一部分。栈分配变量,不像对分配变量,不会导致GC问题,因为它将被在函数返回时摧毁。

Go’s escape analysis is more basic than the HotSpot JVM, for example. The basic rule is that if a reference to a variable is returned from the function where it is declared, it “escapes” - it can be referenced after the function returns, so it must be heap-allocated. This is complicated by:

  • functions calling other functions
  • references being assigned to struct members
  • slices and maps
  • cgo taking pointers to variables

Go语言逃逸分析比HotSpot JVM更基础,例如:基础规则是如果一个变量引用被从它声明的函数返回,它就逃逸了。它可以在函数返回后被引用,因此必须被堆分配,这里有些情况会导致难以理解:

  • 函数调用其他函数
  • 引用被赋值给结构成员
  • slices 和 maps
  • cgo taking poiters to variables

To perform escape analysis, Go builds a graph of function calls at compile time, and traces the flow of input arguments and return values. A function may take a reference to one of it’s arguments, but if that reference is not returned, the variable does not escape. A function may also return a reference, but that reference may be dereferenced or not returned by another function in the stack before the function which declared the variable returns. To illustrate a few simple cases, we can run the compiler with -gcflags '-m', which will print verbose escape analysis information:

执行逃逸分析,Go在编译时建立一个函数调用图,同时跟踪参数输入流和返回值。一个函数可能带一个它自己参数的引用,但是如果引用没有返回,变量不会逃逸。一个函数可能也会返回一个引用,但是那个引用可以被解引用 或者在声明他的函数返回前不被其他在栈里的函数返回。为了描述一个简单的case,我们用参数-gcflags '-m' 执行编译,它将打印详细的分析信息。

 

package main

type S struct {}

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

func identity(x S) S {
  return x
}

 

You’ll have to build this with go run -gcflags '-m -l' - the -l flag prevents the function identity from being inlined (that’s a topic for another time). The output is: nothing! Go uses pass-by-value semantics, so the x variable from main will always be copied into the stack of identity. In general code without references always uses stack allocation, trivially. There’s no escape analysis to do. Let’s try something harder:

你将不得不使用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

The first line shows that the variable “flows through”: the input variable is returned as an output. But identity doesn’t take a reference to z, so the variable doesn’t escape. No references to x survive past main returning, so x can be allocated as part of the stack frame of main.

A third experiment:

第一行显示变量流通过程:输入变量被返回作为输出。但是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

Now there’s some escaping going on. Remember that Go has pass-by-value semantics, so z is a copy of the variable x from mainref return a reference to z, so z can’t be part of the stack frame for ref - where would the reference point when ref returns? Instead it escapes to the heap. Even though main immediately throws away the reference without dereferencing it, Go’s escape analysis is not sophisticated enough to figure this out - it only looks at the flow of input and return variables. It’s worth noting that in this case ref would be inlined by the compiler if we weren’t stopping it.

What if a reference is assigned to a struct member?

现在这里出现一些逃逸。记住Go的值传递语法,因此z 是变量x的拷贝。ref 返回一个z的引用,因此z 不能作为函数ref栈帧的一部分,当ref返回时引用指针在哪里?替代栈空间,它保存在堆空间里。即使main立即在不解引用的情况下抛出引用,Go的逃逸分析不会满足于指出它,它仅仅看着输出流和返回遍历,在这个case中这没有价值,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

 

In this case Go can still trace the flow of references, even though the reference is a member of a struct. Since refStruct takes a reference and returns it, y must escape. Compare with this case:

在这个case中,Go能够跟踪引用流,即使引用的是结构成员。自从refStruct带一个引用同时返回了它,y必须逃逸。和下面这个case比较下:

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
}

Output:

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

Since main takes the reference and passes it to refStruct, the reference never outlives the stack frame where the referenced variable was declared. This and the preceding program have slightly different semantics, but if the second program is sufficient it would be more efficient: in the first example i must be allocated on the stack of main, then re-allocated on the heap and copied as an argument to refStruct. In the second example i is only allocated once, and a reference is passed around.

自从main带有一个引用同时传递它给refStruct,引用不会比引用变量声明的栈帧活的更长。这个程序和前面的程序有一些不同的语法,但是如果第二个程序是足够的,它将更有效。在第一个示例中,我必须分配在main

的栈上,那之后重分配在堆上,同时拷贝一个参数到refStruct。在第二个例子中,i只分配一次,同时一个引用被传递。

A slightly more insidious example:

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

 

The problem here is that y is assigned to a member of an input struct. Go can’t trace that relationship - inputs are only allowed to flow to outputs - so the escape analysis fails and the variable must be heap allocated. There are many documented, pathological cases (as of Go 1.5) where variables must be heap allocated due to limitations of Go’s escape analysis - see this link.

Finally, what about maps and slices? Remember that slices and maps are actually just Go structs with pointers to heap-allocated memory: the slice struct is exposed in the reflect package (SliceHeader). The map struct is harder to find, but it exists: hmap. If these structures don’t escape they’ll be stack-allocated, but the data itself in the backing array or hash buckets will be heap-allocated every time. The only way to avoid this would be to allocate a fixed-size array (like [10000]int).

If you’ve (profiled your program’s heap usage)[http://blog.golang.org/profiling-go-programs] and need to reduce GC time, there may be some wins from moving frequently allocated variables off the heap. It’s also just a fascinating topic: for further reading about how the HotSpot JVM handles escape analysis, check out this paper which talks about stack allocation, and also about detecting when synchronization can be elided.

这的问题是y被赋值给一个输入结构的成员。Go 不能跟踪哪个关系 -- 输入被仅仅允许从流到输出,因此逃逸分析失败,变量在堆分配。这里有许多文档的,问题的cases (as of Go 1.5),在哪里变量必须堆分配因为Go 逃逸分析的限制---link。 

最终,map和slices会怎么样?记住 slices和maps实际上是Go通过指针指向堆分配内存的结构:slice结构被暴露在reflect 包(SliceHeader),map结构更难找,但是它存在:hmap.  如果这些结构不逃逸 他们将被栈分配,但是每次数据它自己再后端数组 或hash 桶中将被堆分配。唯一的方式来避免这的是分配一个固定大小的数组(就像 [10000] int)

如果你需要减少GC时间,从堆里移动走频繁分配的变量会是有效的。它也是一个有趣的主题:读取更多HotSpot JVM处理逃逸分析,点开这篇文章 link,它谈到栈分配,同时也关于当同步可能被忽略时候的检测(detecting)。

 

  • 引用:

http://www.agardner.me/golang/garbage/collection/gc/escape/analysis/2015/10/18/go-escape-analysis.html

以上是关于Golang 逃逸分析的主要内容,如果未能解决你的问题,请参考以下文章

golang freecache源码分析

基于Golang的逃逸分析(Language Mechanics On Escape Analysis)

内存逃逸分析

为何要做逃逸分析

"b = &boy{}" vs "*b = boy{}" 谁不讲武德?golang 逃逸分析入门

"b = &boy{}" vs "*b = boy{}" 谁不讲武德?golang 逃逸分析入门