LoginSignup
22
9

More than 5 years have passed since last update.

Golang エスケープ解析

Last updated at Posted at 2017-08-31

はじめに

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

Links

22
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
9