⚡️

Go における関数のインライン化について

2025/01/06に公開

Go 言語における高度な最適化技術の一つである「関数のインライン化」についてのメモ

インライン化の基本

プログラムで関数を呼び出す際、通常は次のような手順を踏む:

  1. 現在の実行位置を保存
  2. 関数の処理を実行
  3. 元の位置に戻って実行を継続

この一連の流れには若干のオーバーヘッドが発生する。インライン化とは、関数の内容を呼び出し元に直接組み込むことで、このオーバーヘッドを削減する手法である。

具体例

例えば、以下のような単純な関数があるとする:

func add(a, b int) int {
    return a + b
}

インライン化が行われると、実際のコードは以下のように展開される:

result := a + b

Go のインライン化の仕組み

Go のコンパイラは、以下の要素を考慮して関数をインライン化するかどうかを判断する:

  • 関数の規模(小規模な関数が望ましい)
  • 処理の複雑さ(条件分岐やネストが少ないほうが良い)
  • 全体のコードサイズへの影響

Go コンパイラは各関数に「コスト」を設定し、このコストが一定値を超えると、インライン化の対象から外れる。

実例

func small() string {
    s := "hello, " + "world!"
    return s
}

func large() string {
    s := "a"
    s += "b"
    // 多数の処理が続く
    return s
}

small 関数はコストが 7 と低く、インライン化されるが、large 関数はコストが 80 を超えるため、インライン化されない。

関数のインライン化の進化

Go 1.9 より前は、他の関数を呼び出さない「リーフ関数」のみがインライン化の対象だった。Go 1.9 以降では、より多くの関数がインライン化の対象となり、コードの最適化の可能性が広がった。

実践例:sync.Mutex.Lock()

並行処理でよく使用される sync.Mutex のロック処理では、以下のような最適化が行われている:

func (m *Mutex) Lock() {
    // 競合がない場合の処理(インライン化される)
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        return
    }
    // 競合時の処理(別関数として実装)
    m.lockSlow()
}

この実装では:

  • 競合のない一般的なケースは高速に処理
  • 競合が発生した場合は別関数で対応

この最適化により、ロック処理の性能が約 14%向上。

まとめ

  • インライン化は関数呼び出しのオーバーヘッドを削減する最適化技術である
  • Go は関数の「コスト」に基づいてインライン化を判断する
  • Go 1.9 以降、より多くの関数がインライン化の対象となった
  • 頻繁に実行される処理 path(高速パス)を優先的にインライン化することで、全体の性能向上を図ることができる

このように、Go のインライン化システムは、性能向上のために最適な箇所を自動的に判断し、効率的なコード生成を実現している。

参考情報

  1. Go Blog: Compiler Optimizations
    https://go.dev/blog/compiler
    Go のコンパイラ最適化に関する公式ブログ記事。

  2. Proposal: Mid-Stack Inlining in Go 1.9
    https://github.com/golang/go/issues/14768
    ミッドスタックインライン化に関する提案とディスカッション。

  3. sync.Mutex Optimization Example
    https://golang.org/pkg/sync/
    Go の sync パッケージの公式ドキュメント。

  4. Effective Go
    https://go.dev/doc/effective_go
    Go プログラムの設計に役立つガイド。

GitHubで編集を提案

Discussion