Golang でのエスケープ処理とその解析
Golang
Lastmod: 2020-10-19

Golang の memory allocation

Golang は C/C++などと異なり、いい感じに変数のメモリ領域を割り当ててくれる。
例えば、以下のようなコードでも Golang は動作する。

type Hoge struct {}

func NewHoge() *Hoge {
	h := Hoge{}
	return &h
}

NewHoge() 内で生成される構造体値の変数 h の参照がそのまま返戻されている。もし、 hNewHoge 用の関数スタックフレーム内に割り当てられた変数であれば、関数から return したあとに参照するのは不可能になる。が、Golang ではこのような変数の参照を追跡し、自動的にヒープ領域へと割り当てるようコンパイル時に最適化してくれる。そのため上記のコードはコンパイルも通るし、正常に実行される。

この処理はエスケープと呼ばれている。エスケープのおかげで、Golang を書くときに値がどの領域に割り当てられていて、どのように渡してよいのか・いけないのかを気にする必要がほとんど無くなる。こういうところでうっかりミスしがちな自分としては、とてもありがたい。。。

Golang の エスケープ処理を追跡する

ところで、コンパイル時のエスケープの動きを見るには、コンパイラ最適化オプションが使える。上記のようなコードが本当にエスケープされているのか、実際に見てみよう。

CompilerOptimizations · golang/go Wiki

Use -gcflags -m to observe the result of escape analysis and inlining decisions for the gc toolchain.

go build -gcflags "-m -l" target.go

-gcflags "-m" でビルド時のエスケープ解析が有効になる。 また -gcflags "-l" でコード上のインライン展開最適化を無効にする。 インライン展開を無効にするのは、エスケープされる変数が最適化されて解析がうまく行かなくなるのを防ぐため。

対象とするコード(memesc.go)
package main

import "fmt"

func main() {
	hoge := NewHoge()
	fmt.Printf("%#v\n", hoge)
}

type Hoge struct {
	id string
}

func NewHoge() *Hoge {
	h := Hoge{id: "000001"}
	return &h
}

分析開始

$ go build -gcflags "-m -l" memesc.go
# command-line-arguments
./memesc.go:15:2: moved to heap: h
./memesc.go:7:12: main ... argument does not escape
./memesc.go:7:13: hoge escapes to heap
1 行目

本来 NewHoge() 関数内ローカル変数であった h が、返戻値となって外部からの参照を持ちうるために、ヒープ領域へと割り当てられている。

2 行目

main 関数内にはエスケープ対象はいないよ、と言われている。

3 行目

NewHoge() から返ってきた hoge はヒープ領域に割り当てられているよ、と言われている。被参照側視点。

バイナリを実行する

実行しても、特に問題はない。

$ ./memesc
&main.Hoge{id:"000001"}

標準出力ベースだけれど、コンパイル時にどのように最適化されて、どの変数がヒープに移動しているのか見ることができた。


Golang を書き始めて最初に「すごい!」と思ったのがこの仕組みだった。 改めて、Golang の言語仕様をシンプルたらしめるとても良い仕組みの一つだと思う。

とはいえ、本当に何も考えずに変数を定義したり渡したりしていいかというとそうではなく、この仕組みが働いてしまうことでパフォーマンスに少なからず影響を与える箇所がある(関数の引数渡しなど)。なので、コードレビューの際などにはパフォーマンスを考慮する観点としてエスケープのことを頭の隅においておくと良いかもしれない。

参考文献