0% completed
Stack, Heap, and Escape Analysis
On This Page
The Stack and the Heap
Escape Analysis
Measuring Allocations
Reading the Compiler Output
Common Causes of Escape
Measuring Before Changing Code
Using This in an Interview
Key Takeaways
Look at these two functions and decide which one allocates memory that the garbage collector (the part of the runtime that frees unused memory) must later free.
func orderValue(id int64) Order { o := Order{ID: id} return o } func orderPointer(id int64) *Order { o := Order{ID: id} return &o }
The bodies differ by one character, and both are correct Go. In C, the second function would be a bug. It returns the address of a local variable that no longer exists once the function returns. In Go, it is safe, and that safety has a measurable cost.
This page explains where Go stores a value, how the compiler chooses the place, and how to count the allocations in a plain program.
The Stack and the Heap
Each goroutine has its own stack, which is a region of memory that holds the local variables of the function calls in progress. A call reserves room for its variables (its stack frame) by adjusting one register (a small storage location inside the CPU), and returning releases that room just as quickly. The memory is reused by the next call, so the garbage collector never has to free it.
The heap is the region of memory for values whose lifetime is not tied to one function call. Placing a value there is called an allocation. The runtime must find a free block of a suitable size. The garbage collector must later discover that nothing refers to the block and free it. An allocation therefore costs time at the moment it happens and again during garbage collection.
| Stack | Heap | |
|---|---|---|
| Lifetime | Ends when the function returns | Ends when nothing refers to the value |
| Cost to obtain | Adjust one register | Find a free block in the allocator |
| Cost to release | Adjust one register | Work for the garbage collector |
| Belongs to | One goroutine | The whole program |
In Value and Pointer Semantics, returning an Order by pointer cost several times more than returning it by value. That difference is the allocation.
Escape Analysis
A value escapes when it may still be in use after the function that created it returns. Escape analysis is the compiler's check for this. The compiler stores a value on the stack only when it can prove that the value cannot outlive its function. If it cannot prove that, the value goes to the heap, because being wrong would leave a pointer to reused memory.
The Go specification never mentions the stack or the heap. Where a variable lives is a decision of the compiler. That decision does not change what the program means, and it can differ between Go releases. The new function and the & operator do not force a heap allocation, and a plain local variable does not guarantee a stack location.
Measuring Allocations
The function testing.AllocsPerRun calls a function many times and returns the average number of heap allocations per call. It works in a plain program, so the answer to the opening question can be measured directly. The //go:noinline comments stop the compiler from copying each function into its caller, for a reason explained below.
Output:
orderValue: 0 orderPointer: 1
The function orderValue copies its result to the caller, and then its stack frame is released. The function orderPointer returns an address, so the Order must remain valid after the function returns, and it goes to the heap.
Reading the Compiler Output
The compiler reports its escape decisions when it receives the flag -m. The output below is from the program above, trimmed to three lines.
go build -gcflags=-m main.go
./main.go:29:56: can inline main.func1 ./main.go:22:2: moved to heap: o ./main.go:29:56: func literal does not escape
The line moved to heap: o names the variable and the line and column where it was declared. The phrase does not escape marks something the compiler kept on the stack, and can inline is a separate report that the function can be copied into its caller. No line reports a decision inside orderValue, because nothing in it moved to the heap. Passing the flag twice, as -gcflags='-m -m', adds the reasoning, shown here for o only. The name ~r0 is the compiler's name for the first result of the function, so the second line says that the address of o flows into the result.
./main.go:22:2: o escapes to heap in orderPointer: ./main.go:22:2: flow: ~r0 ← &o: ./main.go:22:2: from &o (address-of) at ./main.go:23:9 ./main.go:22:2: from return &o (return) at ./main.go:23:2 ./main.go:22:2: moved to heap: o
Removing the //go:noinline comment from orderPointer changes the measured result to 0 allocations in this program. The compiler can now copy the body of orderPointer into its caller, which is called inlining. Escape analysis can then prove that the pointer is used only to read one field inside the caller. The Order no longer outlives the function that now contains it, so it stays on the stack.
The same source line can allocate or not, depending on how it is called. Inlining runs before escape analysis, so a small constructor that returns a pointer often does not allocate when its caller keeps the pointer local.
Common Causes of Escape
Four situations cause many of the escapes in ordinary code. The program below measures each one, along with a similar form that does not allocate.
Output:
struct stored in an any 1 closure returned 2 closure used locally 0 make, unknown size, large 1 make, unknown size, small 0 make, known size 64 KB 0 make, known size 64 KB + 1 1
Returning a pointer is the first cause, and the previous program has already measured it.
Storing a value in an interface usually needs an allocation, because an interface value holds a pointer to its data. Storing the Order in the any variable copies it to the heap. The parameters of fmt.Println have type any, so a call such as fmt.Println(order) lists its argument as escaping in the -m output. The runtime avoids some of these allocations, for example for integers below 256, which it reads from a table of existing values (runtime/iface.go).
Closures are functions that use variables of the enclosing function. When the closure is returned, it and its captured variable must both outlive the call, which accounts for the 2 allocations. When the closure is used only inside the function, neither one has to go on the heap.
Unknown sizes apply to slices made with a size that is known only at run time, because the compiler cannot reserve stack room for an unknown amount. Go 1.25 relaxed this rule. As of Go 1.27, the compiler gives such a make a 32-byte buffer on the stack (the default in cmd/compile/internal/base/flag.go) and uses it whenever the requested size fits. The small slice, at 32 bytes, therefore allocates nothing, and large allocates once. A slice of known size that does not escape stays on the stack up to 64 KB, the limit named in cmd/compile/internal/ir/cfg.go. One more byte moves it to the heap.
Measuring Before Changing Code
Escape analysis is a reason to understand your code, not a reason to rewrite it. Suppose you change a pointer to a value, or remove an interface, to save an allocation that happens once per request. That change usually produces no difference that a user could notice. It can also make the code harder to read.
The sensible order is to find the allocation that matters first, by measuring a program that is doing real work. A function called a million times per second is worth examining, and a function called once at startup is not. The results also apply only to one compiler version. A result that matters should be checked by a test using testing.AllocsPerRun, rather than assumed to last.
A value that several parts of the code must share should remain a pointer even if that allocates, because correct behavior comes before a saved allocation. Value and pointer semantics are chosen by what the code means, and a measured cost is the only reason to change that choice.
Using This in an Interview
Say that the compiler keeps a value on the stack when it can prove that the value does not outlive its function. Otherwise it places the value on the heap, where the garbage collector must free it. Name go build -gcflags=-m as the way to see these decisions. A likely follow-up asks whether new or & always allocates on the heap. Neither does, because the location depends on escape analysis and on the size of the value, and a pointer that stays local can point to the stack.
Key Takeaways
- A stack value costs almost nothing to create or release, and a heap value costs an allocation and later work for the garbage collector.
- The compiler stores a value on the stack only when it can prove that the value cannot outlive its function.
go build -gcflags=-mprints those decisions, andtesting.AllocsPerRuncounts the allocations in a plain program.- Common causes of escape are returning a pointer, storing in an interface, returning a closure, and sizes known only at run time.
- Inlining runs first, so the same function can allocate in one caller and not in another.
- Change code for an allocation only after a measurement shows that the allocation matters.
Go lets you return the address of a local variable. The compiler checks every such case and moves the value to the heap when it must. That check is automatic and safe, and it can change between releases. The useful skill is to read its result and to change code only where a measurement shows a real cost.
Reading Progress
0%
On This Page
The Stack and the Heap
Escape Analysis
Measuring Allocations
Reading the Compiler Output
Common Causes of Escape
Measuring Before Changing Code
Using This in an Interview
Key Takeaways