DEV Community

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

Posted on

Escape analysis in Go Part-2

Escape analysis in Go Part-2

In this post we will see how Go slices 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

Before we start evaluating how compiler places the slices on stack or heap, it is important to understand what a slice is.

Slices

What are slices

  • A slice is NOT a pointer!
  • A slice is a struct! (kind of)

To be precise a slice is defined as:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
Enter fullscreen mode Exit fullscreen mode

A slice consists of:

  • a pointer that points to the underlying array
  • length = current size of the slice
  • capacity = max size of the slice (think: for the purpose of this slice, the underlying array is of this size)

Again, a slice is a struct!

But, the slice is also special in the way it lets us access the member of the slice; using an array like syntax i.e. slice[low:high:max]

Using slices

There are four operations on slices:

  1. slicing
  2. make
  3. copy
  4. append

make

make creates a new slice for a given length and capacity

slicing

slicing also create a new slice

s2 := s1[2:3] // s1 is an existing slice

When we perform the above slicing, the following happens:

  1. A new slice "s2" is created
  2. values of s2.pntr = s1.pntr + 2
  3. s2.cap = s1.cap
  4. s2.len = 3-2 = 1

Given our knowledge about make and slice, note that the following declaration for s2 is invalid:

s1 := make([]int, 5, 6)
s2 := s1[:7] // !!!panic!!!
// explanation: 
// length = 7-0 = 7 cannot be greater than the capacity = 6
// If we were to create a new slice s2 of capacity 7, we would have to use copy or append.
Enter fullscreen mode Exit fullscreen mode

While all the below are valid:

// https://play.golang.org/p/KgHA0n5CLNG
func desc(slice []int) {
    fmt.Printf("len=%v cap=%v value=%v pntrVal=%v\n", len(slice), cap(slice), slice, &(slice[0]))
}

func main() {   
    s1 := make([]int, 5, 6)
    desc(s1)        // len 5 cap 6
    s2 := s1[0:5]
    desc(s2)        // len 5 cap 6
    s3 := s1[0:1]
    desc(s3)        // len 1 cap 6
    s4 := s3[0:2]
    desc(s4)        // len 2 cap 6
    s5 := s1[0:6]
    desc(s5)        // len 6 cap 6
}
Enter fullscreen mode Exit fullscreen mode

slicing cannot change the capacity of the slice

append

append will create a new slice, and may also create a new underlying array if more capacity is needed

func main() {

    s1 := make([]int, 3, 6)
    s1[0] = 0
    s1[1] = 1
    s1[2] = 2   
    desc(s1)          // len 3 cap 6
    s2 := append(s1, 3, 4, 5) // new slice
    desc(s2)          // len 6 cap 6
    s3 := append(s2, 6)   // new underlying array
    desc(s3)          // len 7 cap 12
}
Enter fullscreen mode Exit fullscreen mode

copy

Copy returns the number of elements copied, which will be the minimum of len(src) and len(dst)



Now that we understand what slices are, lets see how the compiler places the slices on stack or heap

// ** Scenario 1** //

// 

// program that I ran
package del

func f1() {   
   // assume f1 calls each of the other functions
}

func f2(len int) {   
   _ = []int{1,2}           // no // line 13
   _ = make([]int, 1000)    // no
   _ = make([]int, 10000)   // yes >> interesting!
   _ = make([]int, len)     // yes >> tricky!
}

func f3() []int {   
   s1 := []int{1,2}     // yes >> tricky! // line 20
   return s1
}

func f4() [2]int {   
   s1 := [2]int{1,2}    // no
   return s1
}

func f5() *[2]int {   
   s1 := [2]int{1,2}    // yes // line 30
   return &s1
}

func f6(x []int) []int {
    y := x              // no
    return y
}

func f7(x *[]int) []int {
    y := x              // no
    return *y
}

func f8(x []int) *[]int {
    y := x              // yes // line 45
    return &y
}

func f9(x *[]int) *[]int {
    y := x              // no
    return y
}
Enter fullscreen mode Exit fullscreen mode
// escape analysis output
# del
.\del.go:13:13: f2 []int literal does not escape
.\del.go:14:12: f2 make([]int, 1000) does not escape
.\del.go:15:12: make([]int, 10000) escapes to heap
.\del.go:16:12: make([]int, len) escapes to heap
.\del.go:20:15: []int literal escapes to heap
.\del.go:30:4: moved to heap: s1
.\del.go:34:9: leaking param: x to result ~r1 level=0
.\del.go:39:9: leaking param: x to result ~r1 level=1
.\del.go:44:9: leaking param: x
.\del.go:45:2: moved to heap: y
.\del.go:49:9: leaking param: x to result ~r1 level=0

// explanation
For f2, "make([]int, 10000)" and "make([]int, len)" escapes to heap. This implies that the underlying array is created on the heap. The reason for the former is that the compiler was not happy to create such a large stack for f2 and hence decided to keep this large array on the heap instead. The reason for the latter is that the compiler simply doesnt know the length of the underlying array, so decides to keep the array on the heap
For f3, "[]int literal" escape to heap, implying that ONLY the underlying array is created on heap. The actual slice struct is still on the stack and a copy of it is created on the calling function f1 when f3 returns. If f3 did not return the slice (like f2) the underlying array would be created on the f3's stack - this is what happened for line 13 in f2
For f4 and f5, we are dealing with arrays and not slices. So it should be pretty straight foward
For f6, is straight forward with everything on f6's stack
For f7, x is a pointer to the slice struct. it creates a copy of the pointer value (address) into y and return the value of y. y doesnt need to be created on the heap
For f8, y is a new slice struct that pointing to same array as x. Since we are returning the address of y, y needs to be on the heap
For f9, straightforward
*/
Enter fullscreen mode Exit fullscreen mode

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


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
- Part 2: Escape analysis in Go Part-2
- Part 3: Enhancing go program's performance 
- Part 4: Based on readers' requests :)
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
limiu82214 profile image
Nimo

thank you, this post is really helpful for me. :)