インライン化
(原文)
ホットでインライン化されていない関数の呼び出しはしばしば実行時間の無視できない部分を占めることがあります。それらの関数をインライン化することで、小さいですが簡単な高速化を図ることができます。
Rust の関数向けには4種類のインライン属性があります:
- なし: コンパイラ自身がその関数はインライン化されるべきなのかを判断します。これは最適化レベルや関数サイズなどに依存します。もしリンク時最適化を使っていない場合には関数がクレートを跨いでインライン化されることはありません。
#[inline]
: これはクレートの垣根を越えて関数がインライン化されるべきであることを示します。#[inline(always)]
: これはクレートの垣根を越えて関数がインライン化されるべきであることを 強く 示します。#[inline(never)]
: これは関数がインライン化されるべきでないことを 強く 示します。
インライン属性は関数がインライン化される/されないことを保証するものではありません。しかし実際には、#[inline(always)]
は例外的な場合を除きすべての場合においてインライン化を行います。
シンプルなケース
インライン化するのに最適な候補は (a) とても小さい、あるいは (b) 呼び出し場所 (call site) が1つである、といったような関数です。コンパイラはインライン属性がなくともそれらの関数を自身の判断でインライン化することがしばしばあります。ですが、コンパイラはいつも最適な選択をするわけではないので、属性の付与が時々必要となります。
Cachegrind は関数がインライン化されているかどうかを確かめるのに適したプロファイラーです。Cachegrind の出力を見てみてください。最初と最後の行がイベントカウントとしてマーク されていない 場合にのみ、関数がインライン化されていることが分かります。
例:
. #[inline(always)]
. fn inlined(x: u32, y: u32) -> u32 {
700,000 eprintln!("inlined: {} + {}", x, y);
200,000 x + y
. }
.
. #[inline(never)]
400,000 fn not_inlined(x: u32, y: u32) -> u32 {
700,000 eprintln!("not_inlined: {} + {}", x, y);
200,000 x + y
200,000 }
影響が予測できないこともあるため、インライン属性を追加した後には測定し直すべきです。時々、以前はインライン化されていた近くの関数がインライン化されなくなったことで、なんの効果もないことがあります。また、コードが遅くなることもあります。インライン化はコンパイル時間にも影響します。特に、関数の内部表現を複製する、クレートを跨ぐようなインライン化については多くの場合影響があります。
難しいケース
時々、巨大で複数の呼び出し場所を持つが、1つの呼び出し場所だけがホットな関数が生まれます。そのような場合、速度向上のためホットな呼び出し場所をインライン化しつつ、不要なコードの肥大化をさけるためにコールドな(ホットの逆で、稀にしか実行されない)呼び出し場所はインライン化したくないものです。これを処理する方法として、関数を、常にインライン化される (always-inlined) ものと決してインライン化されない (never-inlined) ものに分け後者から前者を呼ぶというものがあります。
例えば、この関数は:
#![allow(unused)] fn main() { fn one() {}; fn two() {}; fn three() {}; fn my_function() { one(); two(); three(); } }
下のように2つの関数に分けられます:
#![allow(unused)] fn main() { fn one() {}; fn two() {}; fn three() {}; // ホットな呼び出し場所ではこれを使う #[inline(always)] fn inlined_my_function() { one(); two(); three(); } // コールドな呼び出し場所 (ホットでない呼び出し場所) にはこれを使う #[inline(never)] fn uninlined_my_function() { inlined_my_function(); } }