Golang エスケープ解析

# はじめに

Goはスタックとヒープの扱いをいい感じにやってくれる。

しかし、ヒープよりもスタックの方が高速なので、なるべくスタックの方を使いたい。

そこで、エスケープ解析を意識してヒープ割当を減らしてみる

[エスケープ解析とは？](https://ja.wikipedia.org/wiki/%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97%E8%A7%A3%E6%9E%90)

# エスケープ解析を出力

```
go build gcflags="-m -l" sample.go
go test gcflags="-m -l" -bench=.
```

- `-m` エスケープ解析を出力
- `-l` 関数のインライン化をオフにする （今回はより簡単な例で説明したいのでオフにする。インライン化の影響でエスケープされないことがあるので）

# 実践

## 例１

最初に↓のベンチマークを実行してみます

```go
package main

import "testing"

func Escape() *int {
	v := 1000
	return &v
}

func NoEscape() int {
	v := 1000
	return v
}

func BenchmarkEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = Escape()
	}
}

func BenchmarkNoEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = NoEscape()
	}
}
```

結果↓

```shell-session

$ go test -bench=. -gcflags "-m -l"
# github.com/Kooooya/ground/escape_analysis
./01_test.go:7: &v escapes to heap
./01_test.go:6: moved to heap: v
.....
BenchmarkNoEscape-4   	2000000000	         1.57 ns/op
BenchmarkEscape-4     	100000000	        15.3 ns/op
```

エスケープしてる方が10倍ほど遅いですね。

```
./01_test.go:7: &v escapes to heap
./01_test.go:6: moved to heap: v
```

こちらが&vがヒープに割り当て（エスケープ）られたというログです。
ヒープに割り当てられなければ、BenchmarkEscapeが6行目のvの値を参照することができません。

## 例２

```go

package main

import "testing"

func Escape() *int {
        s := []int{1, 2, 3, 4, 5}
        y := &s[0]
        return y
}

func NoEscape() int {
        s := []int{1, 2, 3, 4, 5}
        y := s[0]
        return y
}

func BenchmarkEscape(b *testing.B) {
        for i := 0; i < b.N; i++ {
                Escape()
        }
}

func BenchmarkNoEscape(b *testing.B) {
        for i := 0; i < b.N; i++ {
                NoEscape()
        }
}
```

```
# command-line-arguments
./02_test.go:7: &s[0] escapes to heap
./02_test.go:6: []int literal escapes to heap
./02_test.go:12: NoEscape []int literal does not escape
.....
BenchmarkEscape-4     	50000000	        27.9 ns/op
BenchmarkNoEscape-4   	500000000	         3.33 ns/op
```

約８倍ほどの差が出ています。
注目するべきは

```
./02_test.go:7: &s[0] escapes to heap
./02_test.go:6: []int literal escapes to heap
```

スライスがエスケープされています。
Goはスライスの要素の１つでもエスケープされてしまうと、 スライス全体がエスケープされます。


なので、戻り値とするスライスの要素を関数のローカル変数に入れて、そこのポインタを返却する用に変更すると、エスケープされる値が小さくなるので高速化されます。

```go

package main

import "testing"

func LittleEscape() *int {
	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3,......}
	y := s[0]
	return &y
}

func Escape() *int {
	s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3,......}
	return &s[0]
}

func BenchmarkLitteEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		LittleEscape()
	}
}

func BenchmarkEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Escape()
	}
}
```

```
./02_test.go:8: &y escapes to heap
./02_test.go:7: moved to heap: y
./02_test.go:6: LittleEscape []int literal does not escape
./02_test.go:13: &s[0] escapes to heap
./02_test.go:12: []int literal escapes to heap
.....
BenchmarkLitteEscape-4   	  500000	      3104 ns/op
BenchmarkEscape-4        	  100000	     11782 ns/op
```

## 例３

mapにセットしたポインタは必ずエスケープされます。

```go

package main

import "testing"

func Escape() {
	s := map[string]*int{}
	i := 1
	s["a"] = &i
}

func NoEscape() {
	s := map[string]int{}
	i := 1
	s["a"] = i
}

func BenchmarkEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Escape()
	}
}

func BenchmarkNoEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		NoEscape()
	}
}

```

```
./04_test.go:8: &i escapes to heap
./04_test.go:7: moved to heap: i
./04_test.go:6: Escape map[string]*int literal does not escape
./04_test.go:12: NoEscape map[string]int literal does not escape
.....
BenchmarkEscape-4     	10000000	       203 ns/op
BenchmarkNoEscape-4   	10000000	       188 ns/op
```

## 例４

sliceも同様にセットしたポインタは必ずエスケープされます。

```go

package main

import "testing"

func Escape() {
	s := make([]*int, 1)
	i := 1
	s[0] = &i
}

func NoEscape() {
	s := make([]int, 1)
	i := 1
	s[0] = i
}

func BenchmarkEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Escape()
	}
}

func BenchmarkNoEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		NoEscape()
	}
}
```

```
.....
./05_test.go:8: &i escapes to heap
./05_test.go:7: moved to heap: i
./05_test.go:6: Escape make([]*int, 1) does not escape
./05_test.go:12: NoEscape make([]int, 1) does not escape
.....
BenchmarkEscape-4     	100000000	        21.0 ns/op
BenchmarkNoEscape-4   	300000000	         4.72 ns/op
```

こちらは結構違いますね。

## 例５

構造体のフィールドであってもエスケープされるものはされます

```go

package main

import "testing"

type S struct {
	M *int
}

func Escape(y int) (z S) {
	z.M = &y
	return z
}

func NoEscape(y *int) (z S) {
	z.M = y
	return z
}

func BenchmarkEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Escape(i)
	}
}

func BenchmarkNoEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		NoEscape(&i)
	}
}

```

```
# command-line-arguments
./06_test.go:10: &y escapes to heap
./06_test.go:9: moved to heap: y
./06_test.go:14: leaking param: y to result z level=0
.....
BenchmarkEscape-4     	100000000	        17.4 ns/op
BenchmarkNoEscape-4   	500000000	         4.29 ns/op
```

↓の関数がなぜリークしないかというと

```go

func NoEscape(y *int) (z S) {
    z.M = y
    return z
}
```
yは一個前のスタックフレームに乗っている値なので、ヒープ上になくても知っているからです。

## 例６

```go

package main

import "testing"

type S struct {
	M *int
}

func Escape(y *int, z *S) {
	z.M = y
}

func NoEscape(y *int) (z S) {
	z.M = y
	return z
}

func BenchmarkEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var s S
		Escape(&i, &s)
	}
}

func BenchmarkNoEscape(b *testing.B) {
	for i := 0; i < b.N; i++ {
		NoEscape(&i)
	}
}
```

```
.....
./07_test.go:9: leaking param: y
./07_test.go:9: Escape z does not escape
./07_test.go:13: leaking param: y to result z level=0
./07_test.go:21: &i escapes to heap
./07_test.go:19: moved to heap: i
.....
BenchmarkEscape-4     	200000000	         8.18 ns/op
BenchmarkNoEscape-4   	500000000	         2.97 ns/op
```

例５で前のスタックフレームの変数なのでという説明をしましたが、

```go

func Escape(y *int, z *S) {
        z.M = y
}
```

Goのコンパイラは入力と出力の流れでしか解析できないので、こちらはエスケープされます

# まとめ

- gcflags="-m -l"
- GCある言語でも、完全にメモリのことを把握しなくてよいわけではない
- スタック使ってても遅い場合がある。ベンチマークを取る

# Links

- http://blog.rocana.com/golang-escape-analysis
- https://docs.google.com/document/d/1CxgUBPlx9iJzkz9JWkb6tIpTe5q32QDmz8l0BouG0Cw/preview