DEV Community

Cover image for Escape analysis in Go Part-1
HM
HM

Posted on

Escape analysis in Go Part-1

Although there are a few great articles on escape-analysis for go, this post is my attempt to demystify and present the topic in simplicity

Escape analysis in Go Part-1

In this post we will see how Go basic types and structs are assigned to heap or stack by the compiler

Note

I will be running a few scenarios and exposing the compiler escape analysis info using the following command:

go build -gcflags -m -l 
Enter fullscreen mode Exit fullscreen mode

Each scenario is formatted as follows:

// ** Scenario <no> ** // // change: <brief text to describe the scenario> // program that I ran ... // escape analysis output ... // explanation ... 
Enter fullscreen mode Exit fullscreen mode


// ** Scenario 1** //

// not using any pointers // program that I ran package del type S struct { x int } func f1() { x:= S{1} _ = f2(x) } func f2(x S) S { y := x return y } 
Enter fullscreen mode Exit fullscreen mode
// escape analysis output <blank> // explanation Since there are no pointers/references all variables are local to the function stack. No heap allocation required 
Enter fullscreen mode Exit fullscreen mode

// ** Scenario 2 ** //

// change: returning a pointer to a struct created in the called function // program that I ran package del type S struct { x int } func f1() { x:= S{1} _ = f2(x) } func f2(x S) *S { y := x return &y } 
Enter fullscreen mode Exit fullscreen mode
// escape analysis output # del .\del.go:13:2: moved to heap: y // explanation In scenario 2, “y” was moved to heap by the compiler because f2 is returning the reference to y. And upon return stack of f2 will be marked invalid. Thus for f1 to use the reference to y, y must be available even after f2 has finished. 
Enter fullscreen mode Exit fullscreen mode

// ** Scenario 3 ** //

// change: returning a struct created in the called function using a pointer to a struct in the calling function // program that I ran package del type S struct { x int } func f1() { s:= S{1} _ = f2(&s) } func f2(x *S) S { y := *x return y } 
Enter fullscreen mode Exit fullscreen mode
// escape analysis output # del .\del.go:12:9: f2 x does not escape // explanation Here, x does not escape and stays on f1's stack. This is because while f2 is running f1's stack will still be available 
Enter fullscreen mode Exit fullscreen mode

// ** Scenario 4** //

// change: returning a pointer to the struct created in the called function using a pointer to a struct in the calling function // program that I ran package del type S struct { x int } func f1() { s:= S{1} _ = *f2(&s) _ = *f3(&s) } func f2(x *S) *S { y := x return y } func f3(x *S) *S { y := *x // line 18 return &y } 
Enter fullscreen mode Exit fullscreen mode
// escape analysis output # del .\del.go:13:9: leaking param: x to result ~r1 level=0 .\del.go:17:9: f3 x does not escape .\del.go:18:4: moved to heap: y // explanation We have 2 functions f2 and f3, that achieve the same thing i.e. accept a *S and return a *S. In f2, y and x are both references to the stack address in f1. While in f3, y contains the value of x and lives on f3's stack. Now since f1 dereferences what f3 returns, y needs to be available on the heap for f1 to deference. 
Enter fullscreen mode Exit fullscreen mode

// ** Scenario 5 ** //

// change: constructing the struct in the called functions // program that I ran package del type S struct { x int } func f1() { i := 123 _ = *f2(i) _ = f3(&i) _ = *f4(&i) } func f2(x int) *S { y := S{x} // line 16 return &y } func f3(x *int) S { y := S{*x} return y } func f4(x *int) *S { y := S{*x} // line 24 return &y } 
Enter fullscreen mode Exit fullscreen mode
// escape analysis output # del .\del.go:16:4: moved to heap: y .\del.go:19:9: f3 x does not escape .\del.go:23:9: f4 x does not escape .\del.go:24:4: moved to heap: y // explanation Shouldn't be a surprise for why y in f2 and f4 is moved to heap 
Enter fullscreen mode Exit fullscreen mode

// ** Scenario 6 ** //

// change: S.x is not *int type // program that I ran package del type S struct { x *int } func f1() { i := 123 _ = *f2(i) _ = f3(&i) _ = *f4(&i) } func f2(x int) *S { y := S{&x} // line 16 return &y } func f3(x *int) S { y := S{x} // line 20  return y } func f4(x *int) *S { y := S{x} // line 24 return &y } 
Enter fullscreen mode Exit fullscreen mode
// escape analysis output # del .\del.go:15:9: moved to heap: x .\del.go:16:4: moved to heap: y .\del.go:19:9: leaking param: x to result ~r1 level=0 .\del.go:23:9: leaking param: x .\del.go:24:4: moved to heap: y .\del.go:10:4: moved to heap: i // explanation For f2, it should be clear why y is being moved to heap. x is also moved to heap for y to continue referencing it even after f2 returns For f3, y is being returned to f1 where x already exists. So no need to place anything on heap For f4, it should be cleat why y is being moved to heap. i (=x since same reference) is also moved to heap for y to continue referencing it even after f1 returns 
Enter fullscreen mode Exit fullscreen mode

// ** Scenario 7 ** //

// change: function assigns value to a member of the struct, and returns nothing // program that I ran package del type S struct { x *int } func f1() { i := 123 var s S f2(i, s) f3(&i, s) f4(i, &s) f5(&i, &s) } func f2(x int, y S) { y.x = &x // line 18 } func f3(x *int, y S) { y.x = x // line 21 } func f4(x int, y *S) { y.x = &x // line 22 x } func f5(x *int, y *S) { y.x = x // line 26 x } 
Enter fullscreen mode Exit fullscreen mode
// escape analysis output # del .\del.go:17:16: f2 y does not escape .\del.go:20:9: f3 x does not escape .\del.go:20:17: f3 y does not escape .\del.go:23:9: moved to heap: x .\del.go:23:16: f4 y does not escape .\del.go:26:9: leaking param: x .\del.go:26:17: f5 y does not escape .\del.go:10:4: moved to heap: i // explanation For f2, nothing is moved to heap since everything lives on the stack of f2 and isn't required once f2 returns For f3, nothing is moved to heap since everything lives on the stack of f3 and isn't required once f3 returns For f4, y is coming from another stack and for x to be available after f4 returns, x must be moved to the heap For f5, same explanation as f4, x must be moved to heap except that x here refers to i since both are pointers to same address. Hence, it must be moved to heap 
Enter fullscreen mode Exit fullscreen mode

Next: In the next post we will see how slices and maps behave when it comes to escape analysis

Some homework for readers:

Q. What are maps, really?

A. Check this out: https://golang.org/src/runtime/map.go#L114

Q. What are slices?

A. Check this out: https://golang.org/pkg/reflect/#SliceHeader


See you next time,

Harleen.

# Series name: **"Go escape analysis and performance"** ## The series of posts consists of the following parts: - Part 0.1: Stacks and heaps simplified - Part 0.2: What is escape analysis - Part 0.3: go gcflags, memprof, cpuprof and pprof - **Part 1: Escape analysis in Go Part-1** (you are on this page) - Part 2: [Escape analysis in Go Part-2](https://dev.to/mannharleen/escape-analysis-in-go-part-2-mn5) - Part 3: Enhancing go program's performance - Part 4: Based on readers' requests :) 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)