Golang エスケープ解析(Escape Analysis)

はじめに

Goはスタックとヒープの扱いをいい感じにやってくれる。
しかし、ヒープよりもスタックの方が高速なので、なるべくスタックの方を使いたい。
そこで、エスケープ解析を意識してヒープ割当を減らしてみる
エスケープ解析とは?

エスケープ解析を出力

go build gcflags="-m -l" sample.go
go test gcflags="-m -l" -bench=.
  • -m エスケープ解析を出力

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

実践

例1

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

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()
    }
}

結果↓


$ 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の値を参照することができません。

例2


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

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

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

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


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

例3

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


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

例4

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


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

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

例5

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


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

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


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

yは一個前のスタックフレームに乗っている値なので、ヒープ上になくても知っているからです。

例6


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

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


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

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

まとめ

  • gcflags="-m -l"

  • スタック使ってても遅い場合がある。ベンチマークを取る

Links


この記事が気に入ったらサポートをしてみませんか?