Devlion Memo

どことなくそれっぽい新卒の日記

Golangのメモリ周りのメモ

以前、このレポジトリがバズっていた。

GitHub - intel-go/bytebuf: Example of how CL133375 can be utilized to mitigate Go escape analysis limitations.

bytebufが小さなサイズのバッファ用にあらかじめ用意しているbootstrapというバイト列が、エスケープ解析の際に必ずヒープ上に確保されてしまうため、buffer構造体も本来はスタック上に確保できるはずであるが、ヒープ上にエスケープされてしまうという問題があった。
つまり、64byte以下の小さなバッファであっても必ずヒープ上にアロケーションが走ってしまい、パフォーマンスが落ちていた。

この件は既にパッチが当たっており、正しくエスケープがされるよう修正されているのだが、これをきっかけに「なんでヒープに確保することが重たいんだっけ」「どういうときにエスケープされるんだっけ」などのメモリ周りを調べたメモ。

なんでヒープ確保が重たいの

スタックの確保はとてもシンプルで、関数呼び出し時にスタックを伸ばし、関数を抜けた時点でスタックを縮める。
比較して、ヒープ上へ確保する場合は、確保先の探索、GCなど様々な処理が走り、この差がパフォーマンスに効いてくる。
他の関数で参照される、参照を取得する必要があるといった場合を除き、極力エスケープしないようなコードが高速に動作する。

エスケープされる条件

基本的には、

  • 関数内のみで参照される値は、スタック上に確保される
  • そうでないものは、ヒープ上へ確保される

のだが、コンパイラが途中で解析を止め、エスケープする条件が複数ある。

この条件は日々変更が入ったり、今回のような特定の条件下で異なる動作をする場合があるため、網羅することはできないが(本来プログラマが意識するべきではないが)、2015年の時点で以下のリンクなどにまとまっている。

Go Escape Analysis Flaws - Google ドキュメント

Goの公式ドキュメントを読んでも「すべての条件を説明するのは複雑すぎるので、コンパイルフラグで実際にエスケープされるか確認して」と書いてある。 以下のコマンドで確認することが出来る。

$ go build -gcflags '-m' ./main.go

CompilerOptimizations · golang/go Wiki · GitHub

関数のインライン化

直接関係はないが、エスケープ解析に効いてくる最適化の一つに、関数のインライン化の解析がある。

Goのコンパイラは、関数が以下の条件に当てはまるとき、関数を直接、呼び出し元に展開する最適化を行っている。(これも上のコマンドで解析結果を確認できる)

  • コードが80ノード以下(Go1.4以前は40ノード)
  • 関数呼び出し、ループ、ラベル、クロージャ、panic、recover、select、switchなどの複雑な構文を含まない

CompilerOptimizations · golang/go Wiki · GitHub

このインライン化がどうメモリ周りに効いてくるかというと、インライン化が無い場合、関数の引数に値を渡した時点でその値はヒープ上に確保されてしまう。 しかし、インライン化された関数であれば、関数内での値の使用となるため、スタック上の確保で済む。

別の記事で、この最適化を意識したコードの例を考えてみたいと思う。

参考

CompilerOptimizations · golang/go Wiki · GitHub

najeira: Go言語のスタックとヒープ

Escape-Analysis Flaws Go, (Golang) Programming - Blog - Ardan Labs

Allocation efficiency in high-performance Go services · Segment Blog