Garbage collection is a very convenient feature of Go-its automatic memory management makes the code cleaner, Also reduces the possibility of memory leaks. However, since garbage collection requires periodic stopping of the program to collect unused objects, additional overhead will inevitably be added. The Go compiler is smart and automatically determines whether a variable should be allocated on the heap for easy collection in the future, or allocated directly to the function's stack space. For variables allocated on the stack, the difference from variables allocated on the heap is that as the function returns, the stack space will be destroyed, so the variables on the stack are directly destroyed without additional garbage collection overhead.
Go's escape analysis is more basic than HotSpot of the Java virtual machine. The basic rule is that if a reference to a variable is returned from the function in which it is declared, an "escape" occurs. Because it may be used by other content outside the function, it must be allocated on the heap. The following situations will be more complicated:
- Functions calling other functions
- Referencing member variables as structures
- Slicing and mapping
- Cgo Pointer to variable
In order to implement escape analysis, Go will construct a function call graph during the compilation phase, while tracking the process of input parameters and return values. If a function only references a parameter, but the reference does not return the function, the variable will not escape. If a function returns a reference, but the reference is released by another function on the stack or does not return the reference, there is no escape. In order to demonstrate several examples, you can add the -gcflags '-m'
parameter during compilation. This parameter will print the detailed information of escape analysis:
package main type S struct {} func main() { var x S _ = identity(x) } func identity(x S) S { return x }
You can execute go run -gcflags '-m -l'
(Note: the go code file name is omitted in the original text) to compile this code. The -l parameter prevents function identity
from being inlined (we will discuss inlining another time) this topic). You will see no output! Go uses value transfer, so the x
variable in the main
function will always be copied to the stack space of the function identity
. Usually code that does not use references allocates memory through stack space. So no escape analysis is involved. Let's try the more difficult one:
package main type S struct {} func main() { var x S y := &x _ = *identity(y) } func identity(z *S) *S { return z }
The corresponding output is:
./escape.go:11: leaking param: z to result ~r1 ./escape.go:7: main &x does not escape
The first line shows the "flow" of the variable z
: the input parameter is directly used as The return value is returned. However, function identity
did not take away the reference z
, so no variable escape occurred. After the main
function returns, no reference to x
exists, so the x
variable can be allocated memory in the stack space of the main
function.
The third experiment:
package main type S struct {} func main() { var x S _ = *ref(x) } func ref(z S) *S { return &z }
The output is:
./escape.go:10: moved to heap: z ./escape.go:11: &z escapes to heap
Now there is an escape. Remember that Go is pass-by-value, so z
is a copy of the variable x
. Function ref
returns a reference to z
, so z
cannot be allocated on the stack, otherwise when function ref
returns, the reference will Where does it point? So it escaped into the heap. In fact, after executing ref
and returning to the main
function, the main
function discards the reference instead of dereferencing it, but Go's escape analysis is not smart enough to identify it. This situation.
It's worth noting that in this case, if we don't stop the reference, the compiler will inline ref
.
What happens if the structure members are defined as references?
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 }
The output is:
./escape.go:12: moved to heap: y ./escape.go:13: &y escapes to heap
In this case, even though the reference is a member of the structure, Go still tracks the flow of the reference. Since function refStruct
accepts a reference and returns it, y
must escape. Compare the following example:
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 }
The output is:
./escape.go:12: leaking param: y to result z ./escape.go:9: main &i does not escape
Although the i
variable is referenced in the main
function and passed In function refStruct
, but the scope of this reference does not exceed the stack space where it is declared. This is slightly different in semantics from the previous program, and this one will be more efficient: in the previous program, the variable i
must be allocated on the stack of the main
function and then used as a parameter Copy it to function refStruct
, and allocate the copied copy on the heap. In this example, i
is assigned only once and then the reference is passed around.
Let’s look at a somewhat convoluted 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 }
The output is:
./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的逃逸分析的局限性,许多变量会被分配到堆上,请参考此链接,这里面记录了许多案例(从Go1.5开始)。
最后,来看下映射和切片是怎样的呢?请记住,切片和映射实际上只是具有指向堆内存的指针的Go结构:slice
结构是暴露在reflect
包中(SliceHeader
)。map
结构就更隐蔽了:存在于hmap。如果这些结构体不逃逸,将会被分配到栈上,但是其底层的数组或者哈希桶中的实际数据会被分配到堆上去。避免这种情况的唯一方法是分配一个固定大小的数组(例如[10000]int
)。
如果你剖析过你的程序堆使用情况(https://blog.golang.org/pprof
),并且想减少垃圾回收的消耗,可以将频繁分配到堆上的变量移到栈上,可能会有较好的效果。进一步研究HotSpot JVM是如何进行逃逸分析的会是一个不错的话题,可以参考这个链接,这个里面主要讲解了栈分配,以及有关何时可以消除同步的检测。
更多golang相关技术文章,请访问golang教程栏目!