プログラミングでは、関数を連鎖させます。関数Aが関数Bを呼び出します。そして同様に続きます。

このようにプログラミングする必要はありません。単一の関数を使ってプログラム全体を書くこともできます。単一の関数を使って非自明なプログラムを書くのは楽しい練習になるでしょう…ただし、コードの作成をAIに委ねる限り、人間はすぐに長い関数に苦労するからです。

主要なコンパイラ最適化の1つが「インライン化」です。コンパイラは関数の定義を取得し、呼び出し箇所でそれを代入しようとします。概念的には非常に単純です。関数add3が関数addを呼び出す次の例を考えてみましょう。

int add(int x, int y) {
    return x + y;
}

int add3(int x, int y, int z) {
    return add(add(x, y), z);
}

呼び出しを手動でインライン化すると次のようになります。

int add3(int x, int y, int z) {
    return x + y + z;
}

関数呼び出しはパフォーマンス的にはかなり安価ですが、無料ではありません。関数が自明でないパラメータを取る場合、それらをスタックに保存および復元する必要があるため、余分なロードとストアが発生します。関数内にジャンプし、最後にそこからジャンプアウトする必要があります。また、システム上の関数呼び出し規約と使用している命令の種類によっては、最初と最後に追加の命令が必要になります。

関数が私のadd関数のように十分に単純な場合、パフォーマンスが重要な場合には常にインライン化する必要があります。具体的な例を見てみましょう。配列内の整数を合計してみましょう。

for (int x : numbers) {
  sum = add(sum, x);
}

MacBook(LLVM搭載のM4プロセッサ)を使用しています。

function ns/int
regular 0.7
inline 0.03

すごい。インライン版は20倍以上高速です。

何が起こっているのかを見てみましょう。「add」関数の呼び出しサイトは、関数への呼び出しを伴う単純なループです。

ldr    w1, [x19], #0x4
bl     0x100021740    ; add(int, int)
cmp    x19, x20
b.ne   0x100001368    ; <+28>

関数自体は可能な限り安価です。わずか2命令です。

したがって、各加算に6命令を費やします。加算あたり約3サイクルかかります。

インライン関数の場合はどうでしょうか?

ldp    q4, q5, [x12, #-0x20]
ldp    q6, q7, [x12], #0x40
add.4s v0, v4, v0
add.4s v1, v5, v1
add.4s v2, v6, v2
add.4s v3, v7, v3
subs   x13, x13, #0x10
b.ne   0x1000013fc    ; <+104>

全く異なります。コンパイラは加算を、8命令で16個の整数をブロック単位で処理する高度な(SIMD)命令に変換しました。したがって、命令数は1整数あたり0.5命令(6命令から)になりました。命令数が12分の1になりました。命令数が少ないだけでなく、プロセッサは1サイクルあたりにより多くの命令を完了できるため、大幅なパフォーマンス向上が得られます。

インライン化を維持したまま、コンパイラがこれらの高度な命令を使用できないようにするとどうでしょうか?それでも大幅なパフォーマンス向上が得られます(約10倍高速)。

function ns/int
regular 0.7
inline 0.03
inline (no SIMD) 0.07

わかりました。しかしadd関数は極端な例です。常にインライン化すべきであることはわかっています。文字列内のスペースの数をカウントする関数のような、あまり自明でないものについてはどうでしょうか。

size_t count_spaces(std::string_view sv) {
    size_t count = 0;
    for (char c : sv) {
        if (c == ' ') ++count;
    }
    return count;
}

文字列が適度に長い場合、関数呼び出しのオーバーヘッドは無視できるはずです。
1000文字の文字列を渡してみましょう。

function ns/string
regular 111
inline 115

インライン版は高速になるだけでなく、わずかに遅くなっています。理由はわかりません。

短い文字列(0〜6文字程度)を使用するとどうでしょうか?その場合、インライン関数の方が測定可能なほど高速です。

function ns/string
regular 1.6
inline 1.0

要点:

  1. 短くて単純な関数は、パフォーマンスが懸念される場合には可能な限りインライン化すべきです。利点は印象的です。
  2. 高速または低速になり得る関数では、インライン化するかどうかの決定は入力によって異なります。文字列処理関数の場合、文字列のサイズが、最高のパフォーマンスを得るためにインライン化が必要かどうかを決定する可能性があります。

: ソースコードはこちらで入手できます