The Command
To view the compiler’s escape analysis decisions, you use the -gcflags flag with the -m option when building or testing your code.
go build -gcflags -m=2
• -gcflags: Passes flags to the Go compiler.
• -m: Triggers the printing of optimization decisions, including escape analysis and inlining.
• Level 2 (-m=2): Providing the flag twice (or using =2) increases the verbosity. While there are up to 4 levels, level 2 is recommended as it provides sufficient detail without being overwhelming.
Interpreting the Output
The output will list specific lines in the code and tell you the compiler’s decision. You should look for phrases like:
• escapes to heap
• moved to heap
Example log
The following garbage collector log came out of using the function SlowJoin that looks like this:
func SlowJoin(input []string) string {
for i, s := range input {
if i > 0 {
result += ", "
}
result += s
}
return result
}
A part of GC output when calling the function:
<autogenerated>:1: inlining call to reflect.flag.ro
<autogenerated>:1: inlining call to reflect.flag.ro
./join.go:9:15: parameter input leaks to ~r0 for SlowJoin with derefs=1:
./join.go:9:15: flow: {temp} ← input:
./join.go:9:15: flow: s ← *{temp}:
./join.go:9:15: from for loop (range-deref) at ./join.go:14:14
./join.go:9:15: flow: result ← s:
./join.go:9:15: from result += s (assign) at ./join.go:18:10
./join.go:9:15: flow: ~r0 ← result:
./join.go:9:15: from return result (return) at ./join.go:20:2
./join.go:9:15: leaking param: input to result ~r0 level=1
The compiler is tracing the life of the input slice like a detective:
flow: s ← *{temp}: It sees the function pulling strings out of theinputslice.flow: result ← s: It sees the function taking those strings and merging them into theresultvariable.flow: ~r0 ← result: It sees the function returning that result to the outside world.
leaking param: input to result ~r0 level=1: Because the result (which contains data from input) is sent out of the function, the compiler “gives up” on keeping it on the stack. It must allocate memory on the heap to ensure the returned string doesn’t disappear when the function ends.
In a high-performance Go function, you want your variables to stay on the Stack (fast, zero-cleanup). In SlowJoin, the compiler is explicitly saying: “I cannot keep this on the stack.”
- Every time that loop runs, the
result += sline triggers a new heap allocation because the compiler has already flaggedresultas a “leaking” object.
Another example of a GC Log
This is a Go garbage collector (GC) log message indicating a forced garbage collection cycle.
GC forced
gc 25 @721.284s 0%: 2.4+20+0.006 ms clock, 28+0/45/0+0.079 ms cpu, 84->84->82 MB, 167 MB goal, 0 MB stacks, 0 MB globals, 12 P
Let’s break down what each part means:
Key Components:
gc 25- This is the 25th GC cycle since the program started.@721.284s- Occurred 721 seconds (~12 minutes) after program start.0%- GC overhead: 0% of CPU time spent on garbage collection (very low, which is good).
Timing Breakdown:
2.4+20+0.006 ms clock
- Wall clock time:
- 2.4ms: Stop-the-world sweep termination.
- 20ms: Concurrent mark/scan phase.
- 0.006ms: Stop-the-world mark termination.
28+0/45/0+0.079 ms cpu
- CPU time across all processors (12 P)
Memory Stats:
84->84->82 MB:
- 84 MB: Heap size before GC.
- 84 MB: Heap size after GC (before sweep).
- 82 MB: Live heap after GC.
167 MB goal - Target heap size before next GC.
12 P - 12 processors available.
What “GC forced” Means:
The GC was manually triggered rather than automatically triggered by memory pressure. This could happen from:
-
runtime.GC()call in code. -
External monitoring/profiling tools.
-
GODEBUG=gctrace=1environment variable.
Common Reasons for Escaping
• Sharing Up the Stack: Returning the address of a local variable (e.g., return &u) forces the variable to the heap because it must exist after the function returns.
• Interface conversion: Passing a concrete value to a function that accepts an interface (like io.Reader or fmt.Println) often causes an escape because the compiler cannot determine at compile time how the value will be used.
• Global Variables: Storing a pointer to a local variable in a global variable makes it reachable after the function ends, forcing an allocation.
• Unknown Size: Variables with sizes not known at compile time (e.g., slices created with a variable length make([]byte, size)) must be allocated on the heap.
Comments