MJHD

エモさ駆動開発

Golangメモリ周りのメモ (mid-stack inlining編)

前回

mjhd.hatenablog.com

mid-stack inliningとは

mid-stack inliningとは、前回の記事でも紹介したインライン化をより積極的に行うようになる機能のことである。

前回の記事からの引用だが、

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

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

以上がインライン化の大まかな条件であった。ここには、関数の呼び出しという項目があり、今まで関数のインライン化は一番末端にある関数(木構造でいう葉っぱ)でしかインライン化を行なっていなかった。
mid-stack inliningは、これを改良し末端以外の関数でもインライン化を行うための最適化である。

インライン化による弊害

これはmid-stack inliningの有効無効に関わらない話だが、インライン化により、スタックトレースを出力した際などに実際のソースコードとの差異が出てしまう。

例えば以下のような関数があったとする。

type s struct {
  c int
}

func a(v *s) {
    v.c++
}

func b(v *s) {
    a(v)
}


func main() {
    b((nil)(*s))
}

このコードがもしインライン化されたとすれば、以下のようなコードになる。

type s struct {
  c int
}

func main() {
    ((*s)(nil)).c++
}

ここでnil pointer dereferenceが発生した場合、以下のようなスタックトレースが表示されてしまう。

panic: nil pointer dereference

main.main()
        /main.go:16 +0x2

関数a, bの情報が消えており、デバッグが困難になってしまう。

どうやって解決するの?

mid-stack inliningでは、インライン化した関数のPCとソースコード上の位置を保持する木構造をテーブルとして表した構造体(InlTree)を生成し、シンボルテーブルに格納する。
実行時は格納されたテーブルから実際のスタックトレースを生成し表示を行うことで、インライン化をしたとしても正しく出力が行えるようになる。 ただし制限として、以下のように引数は省略されてしまう。

panic: runtime error: invalid memory address or nil pointer dereference

main.a(...)
         /main.go:8
main.b(...)
         /main.go:12
main.main()
        /main.go:16 +0x2

どのぐらい早くなるの?

ベンチマークによると9%ほどパフォーマンスが改善するらしい。
また、標準ライブラリにもコードを少し調整することでmid-stack inliningの恩恵を受けることのできる部分があるため、以下のような改善を繰り返していくことでGo全体のパフォーマンスが向上するのではと思う。

sync.Once.Do https://go-review.googlesource.com/c/go/+/156362/

sync.Mutex.Unlock https://go-review.googlesource.com/c/go/+/148958

参考文献

talk: Mid-stack inlining in the Go compiler (external) - Google スライド cmd/compile: enable mid-stack inlining · Issue #19348 · golang/go · GitHub proposal/19348-midstack-inlining.md at master · golang/proposal · GitHub

次回

mjhd.hatenablog.com