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”
Each scenario is formatted as follows:
// ** Scenario <no> ** //
// change: <brief text to describe the scenario>
// program that I ran
...
// escape analysis output
...
// explanation
...
Before we start evaluating how compiler places the slices on stack or heap, it is important to understand what a slice is.
Slices
- ref: https://blog.golang.org/go-slices-usage-and-internals
- ref: https://golang.org/pkg/reflect/#SliceHeader
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
}
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:
- slicing
- make
- copy
- 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:
- A new slice "s2" is created
- values of s2.pntr = s1.pntr + 2
- s2.cap = s1.cap
- 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.
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
}
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
}
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
}
// 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
*/
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 :)
Top comments (1)
thank you, this post is really helpful for me. :)