Search code examples
gocompiler-optimizationescape-analysis

What is the meaning of the output from 'go run -gcflags -m xxx.go'


Trying to check whether a local variable is allocated on heap or stack in go program, and can't be sure the meaning of some output from go's gc.


Code

variable_heap_stack.go:

// variable heap & stack learn,
// run with:
//  go run -gcflags -m xxx.go
package main

import "fmt"

func getPointerOfLocalVar() *int {
    x := 10 // go will put it into heap,
    return &x
}

// heap & stack test,
func heapStackTest() {
    px := getPointerOfLocalVar()
    fmt.Printf("x: %d\n", *px)

    y := 20 // go will put it into stack,
    fmt.Printf("y: %d\n", y)
}

func main() {
    heapStackTest()
}

Execute:

go run -gcflags -m variable_heap_stack.go

Output:

# command-line-arguments
./variable_heap_stack.go:8:6: can inline getPointerOfLocalVar
./variable_heap_stack.go:15:28: inlining call to getPointerOfLocalVar
./variable_heap_stack.go:10:9: &x escapes to heap
./variable_heap_stack.go:9:6: moved to heap: x
./variable_heap_stack.go:16:24: *px escapes to heap
./variable_heap_stack.go:19:13: y escapes to heap
./variable_heap_stack.go:15:28: heapStackTest &x does not escape
./variable_heap_stack.go:16:12: heapStackTest ... argument does not escape
./variable_heap_stack.go:19:12: heapStackTest ... argument does not escape
x: 10
y: 20

Questions

  • What does escapes to heap mean? Is it going to heap or not?
  • moved to heap, this means move to heap, right? What's the difference with the above one?
  • The y variable is local, no one refer to it after function returns, but there still got a line y escapes to heap, why was that?

Solution

  • What does escapes to heap mean? Is it going to heap or not?

    This means the value indicated in the message leaves the "boundaries" of the function, and as such, it cannot be guaranteed what happens with it outside of the function, so if the value is a pointer or reference (but only then), the pointed or referenced value must be allocated on the heap.

    You can think of escapes to heap as a debug message, it does not indicate that one of your variables is "relocated" to the heap.

    So to put it simple, "escapes to heap" is analogous to the term: "it leaves the function", or "it is passed outside of the function".

    As an example this line:

    ./variable_heap_stack.go:16:24: *px escapes to heap
    

    Says that the value *px is passed outside of the function, namely as an argument to fmt.Printf() in this line:

    fmt.Printf("x: %d\n", *px)
    

    moved to heap, this means move to heap, right? What's the difference with the above one?

    This indicates that the compiler decided to move the variable indicated in the message to the heap, because it might be referenced outside of the function, and thus it must survive the function. And since stack-allocated values may become invalid once you return from the function, for the indicated variable to be valid after the function returns, it must be on the heap.

    Moved to heap is a direct announcement that one of your variables was indeed "relocated" to the heap. Note: "relocated" means that the variable will be allocated on the heap in the first place, actual "relocation" will not happen in any case.

    The y variable is local, no one refer to it after function returns, but there still got a line y escapes to heap, why was that?

    As mentioned before, this does not mean y is relocated to heap, it only means the value y is passed outside of the function, namely as a parameter to fmt.Printf() in this line:

    fmt.Printf("y: %d\n", y)
    

    y will not be moved to heap just because of this, there is no need, as it is passed to fmt.Printf() by making a copy of its value, and fmt.Printf() will have no way of reaching your y local variable.

    Note that since fmt.Printf() expects a value of type ...any, the variable y will be wrapped in an interface value, then in a slice, and wrapping it in an interface value is what causes the "escape". If you'd pass it to a function expecting int or even ...int, no escape would happen as that would simply pass the int value without any reference or connection to the y variable whatsoever.

    Tip:

    You can get more details about optimization decisions and escape analysis by passing -m twice like this:

    go run -gcflags='-m -m' variable_heap_stack.go
    

    Then the output of this command will be:

    ./variable_heap_stack.go:8:6: can inline getPointerOfLocalVar as: func() *int { x := 10; return &x }
    ./variable_heap_stack.go:14:6: cannot inline heapStackTest: non-leaf function
    ./variable_heap_stack.go:15:28: inlining call to getPointerOfLocalVar func() *int { x := 10; return &x }
    ./variable_heap_stack.go:22:6: cannot inline main: non-leaf function
    ./variable_heap_stack.go:10:9: &x escapes to heap
    ./variable_heap_stack.go:10:9:         from ~r0 (return) at ./variable_heap_stack.go:10:2
    ./variable_heap_stack.go:9:2: moved to heap: x
    ./variable_heap_stack.go:16:24: *px escapes to heap
    ./variable_heap_stack.go:16:24:        from ... argument (arg to ...) at ./variable_heap_stack.go:16:12
    ./variable_heap_stack.go:16:24:        from *(... argument) (indirection) at ./variable_heap_stack.go:16:12
    ./variable_heap_stack.go:16:24:        from ... argument (passed to call[argument content escapes]) at ./variable_heap_stack.go:16:12
    ./variable_heap_stack.go:19:13: y escapes to heap
    ./variable_heap_stack.go:19:13:        from ... argument (arg to ...) at ./variable_heap_stack.go:19:12
    ./variable_heap_stack.go:19:13:        from *(... argument) (indirection) at ./variable_heap_stack.go:19:12
    ./variable_heap_stack.go:19:13:        from ... argument (passed to call[argument content escapes]) at ./variable_heap_stack.go:19:12
    ./variable_heap_stack.go:15:28: heapStackTest &x does not escape
    ./variable_heap_stack.go:16:12: heapStackTest ... argument does not escape
    ./variable_heap_stack.go:19:12: heapStackTest ... argument does not escape
    x: 10
    y: 20