Understanding Escape Analysis in Go
Go uses escape analysis to decide whether values are stored in the stack or heap. Escape to heap happens when a value needs a longer lifespan or exceeds stack size.
Join the DZone community and get the full member experience.
Join For FreeGo uses escape analysis to determine the dynamic scope of Go values. Typically, go tries to store all the Go values in the function stack frame. The go compiler can predetermine which memory needs to be freed and emits machine instructions to clean it up. This way it becomes easy to clean up memory without the intervention of the Go Garbage Collector. This way of allocating memory is typically called stack allocation.
But when the compiler cannot determine the lifetime of a Go value it escapes to the heap. A value may also escape to the heap when the compiler does not know the size of the variable, or it’s too large to fit into the stack, or if the compiler cannot determine whether the variable is used after the function ends or the function stack frame is not used anymore.
Can we truly and completely know whether the value is stored in the heap or stack? The answer is NO. Only the compiler would know where exactly the value is stored all the time. As mentioned in this doc “The Go language takes responsibility for arranging the storage of Go values; in most cases, a Go developer need not care about where these values are stored, or why, if at all.”
There might still be scenarios where we might want to know the allocation to improve performance. As we know, physical memory is finite, and overuse might result in unnecessary performance issues.
Let’s now see how we can determine when and why a variable escapes to the heap. We will use the go build
command to determine it. Run go help build
to get various options for go build
. We will use go build -gcflags=”-m”
command to ask the compiler where the variables are being put. Let’s now go through some examples to determine it:
- In this example, we call a square function from our main function
package main
func main() {
x := 2
square(x)
}
func square(x int) int {
return x*x
}
When we run the above code with go build -gcflags=”-m”
we get the below result:
# github.com/pranoyk/escape-analysis
./main.go:8:6: can inline square
./main.go:3:6: can inline main
./main.go:5:8: inlining call to square
Right now everything is in the stack frame.
2. Let’s now modify our code to return a pointer from the square function
package main
func main() {
x := 2
square(x)
}
func square(x int) *int {
y := x*x
return &y
}
When we build this code we get the following:
# github.com/pranoyk/escape-analysis ./main.go:8:6: can inline square ./main.go:3:6: can inline main ./main.go:5:8: inlining call to square ./main.go:9:2: moved to heap: y
Here the value `y`
escaped to the heap. Now notice why this happened. The value of `y`
has to prevail once the square function life cycle has finished and hence it escapes to the heap.
3. Let’s modify the above function. Let’s make our square function accept a pointer and not return a value.
package main
func main() {
x := 4
square(&x)
}
func square(x *int) {
*x = *x**x
}
When we build the above code we get the following:
# github.com/pranoyk/escape-analysis
./main.go:8:6: can inline square
./main.go:3:6: can inline main
./main.go:5:8: inlining call to square
./main.go:8:13: x does not escape
Notice in the above function even though we are passing the pointer to the square the compiler mentions that the variable `x`
does not escape. This is because the variable `x`
is created in the main function stack frame which lives longer than the square function stack frame.
4. Let’s make one more modification to the above code. Let’s make our square function both accept and return a pointer.
package main
func main() {
x := 4
square(&x)
}
func square(x *int) *int {
y := *x**x
return &y
}
The allocation of the above code is:
# github.com/pranoyk/escape-analysis
./main.go:8:6: can inline square
./main.go:3:6: can inline main
./main.go:5:8: inlining call to square
./main.go:8:13: x does not escape
./main.go:9:2: moved to heap: y
Now notice carefully that the result of this code is a combination of examples 2 and 3. If we look even further closely we can say that sharing memory down from the main to another function typically stays on the stack and sharing memory up from a function to the main typically escapes to the heap. We can never be completely sure about it always because only the compiler would truly know where the value is stored. But this still gives some hint as to when an escape to the heap may occur.
Conclusion
- Escape analysis in Go is a way in which the compiler determines whether the value is to be stored in the stack frame or the heap.
- Anything that cannot be stored in the function stack frame escapes to the heap.
- We can check the memory allocation of our code using
`go build -gcflags=”-m”`
. - Although, go manages the memory allocation quite efficiently and almost always a developer might not be concerned with it. It’s still good to know in case you want to improve on the performance.
References
Published at DZone with permission of Pranoy Kundu. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments