Stack, Heap and Escape Analysis
The Go compiler uses escape analysis to determine which variables should be allocated on the stack of a goroutine and which variables should be allocated on the heap.
The way a variable is used - not declared - determines whether it lives on the stack or the heap.
Sharing up (returning pointers) typically escapes the Heap.
Go encourages the use of value types, which are allocated on the stack, as they don't require garbage collection.
The compiler will try to allocate a variable to the local stack frame of the function in which it is declared. However, it is also able to perform escape analysis: if it cannot prove that a variable is not referenced after the function returns, then it allocates it on the heap instead.
Output: 42
package main
import (
"fmt"
)
func f() *int {
a := 42
return &a
}
func main() {
defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop()
fmt.Printf("%d", *f())
}

The heap footprint increases with time. At a certain point, the GC runs and cleans up the heap, before it starts growing again.
We can rewrite the program so that there is no reference to a
outside of the stack frame, then the variable doesn’t escape to the heap.
package main
import (
"github.com/pkg/profile"
)
func f() int {
a := 42
return a
}
func main() {
defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop()
for i := 0; i < 1000000; i++ {
f()
}
}
// go build -o ./main -gcflags=-m ./main.go
// # command-line-arguments
// ./main.go:7:6: can inline f
// ./main.go:15:4: inlining call to f
// ./main.go:13:21: ... argument does not escape

a
does not escape to the heap, since it is not modified “above” f
’s stack frame, (only “below”, in g
’s).
package main
func g(a *int) {
*a++
}
func f() int {
a := 42
g(&a)
return a
}
func main() {
for i := 0; i < 1000000; i++ {
f()
}
}
// go build -o ./main -gcflags=-m ./main.go
// # command-line-arguments
// ./main.go:9:6: can inline g
// ./main.go:13:6: can inline f
// ./main.go:15:3: inlining call to g
// ./main.go:22:4: inlining call to f
// ./main.go:22:4: inlining call to g
// ./main.go:9:8: a does not escape
If we execute g
in a goroutine, we find that a
escapes to the heap.
package main
func g(a *int) {
*a++
}
func f() int {
a := 42
go g(&a) // yes, this is a race condition, but this is just a demo :)
return a
}
func main() {
for i := 0; i < 1000000; i++ {
f()
}
}
// go build -o ./main -gcflags=-m ./main.go
// # command-line-arguments
// ./main.go:3:6: can inline g
// ./main.go:3:8: a does not escape
// ./main.go:8:2: moved to heap: a
This is because each goroutine has its own stack. As a result, the compiler cannot guarantee that f
’s stack hasn’t been popped (invalidating a
) when g
accesses it. Therefore, the variable must live on the heap.
space for the execution of thread;
when a function in called, a block
(stack frame) is reserved on top of the stack for local variables and some bookkeeping data;
when a function returns, the block becomes unused and can be used the next time any function is called
requires manual housekeeping of what memory is to be reserved and what is to be cleaned the memory allocator will perform maintenance tasks such as defragmenting allocated memory or garbage collecting
initial stack memory allocation is done by the OS at compile time
allocated at run time
The function main
has its local variables n and n2. main
function is assigned a stack frame in the stack. Same goes for the function square
.

Now as soon as the function square
returns the value, the variable n2
will become 16
and the memory function square
is NOT cleaned up, it will still be there and marked as invalid (unused).

Now when the Println
function is called, the same unused
memory on stack left behind by the square function is consumed by the Println
function.

Resources:
Last updated
Was this helpful?