Understanding Goa Escape Analysis

Go uses output parsing to determine the dynamic range of Go values. Typically, go tries to store all Go values ​​in function bundle framework. The Go compiler can determine in advance which memory needs to be freed and emits machine instructions to clean it up. In this way, it becomes easy to clean the memory without the intervention of the Go Garbage Collector. This method of allocating memory is usually called distribution of stacks.

But when the compiler can’t determine Go’s lifetime, value it run away in a heap. A value can also escape onto the heap when the compiler does not know the size of the variable, or it is too large to fit on the stack, or if the compiler cannot determine whether the variable is used after the function ends or the function stack frame is no longer used.

Can we truly and completely know if a value is stored in a heap or a stack? The answer is NO. Only the compiler would know exactly where the value is stored all the time. As stated in this doc “The Go language takes responsibility for organizing the storage of Go values; in most cases the Go developer does not need to worry about where these values ​​are stored or why, if at all.”

There may still be scenarios where we would like to know the distribution to improve performance. As we know, physical memory is limited, and overuse can lead to unnecessary performance problems.

Now let’s see how we can determine when and why a variable escapes to the heap. We will use each other go build an order to determine it. Jogging go help build to get various options for go build. We will use each other go build -gcflags=”-m” command to ask the compiler where to put variables. Let’s now go through some examples to determine this:

  1. In this example, we call the quadratic 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 following 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 within the stack.

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 is the value `y` ran away in a heap. Now notice why this happened. Value `y` it must prevail after it completes the life cycle of the quadratic function and therefore escapes to the heap.

3. Let’s modify the above function. Let 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 code above, 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 that in the above function, even though we are passing a pointer to a square, the compiler mentions that the variable `x` don’t run away. This is because the variable `x` is created in the main stack function frame that lives longer than the square stack function frame.

4. Let’s make another change to the code above. Let our square function accept and return a pointer.

package main


func main() 
   x := 4
   square(&x)



func square(x *int) *int 
   y := *x**x
   return &y

The assignment 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

Notice carefully that the result of this code is a combination of examples 2 and 3. If we take a closer look, we can say that the memory sharing of main to another function usually stays on the stack and sharing memories from a the function on main typically escapes to the heap. We can never be completely sure of this because only the compiler would really know where the value is stored. But this does give some indication of when a runaway in the pile might happen.

Conclusion

  • Escape analysis in Go is how the compiler determines whether a value should be stored in a stack frame or heap.
  • Anything that cannot be stored in the frame therefore function escapes to the heap.
  • We can check the memory allocation of our code using `go build -gcflags=”-m”`.
  • Although, go manages memory allocation quite efficiently and almost always the developer may not be concerned about it. Still good to know if you want to improve performance.

References

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *