ビルド設定
Rust プログラムは、コードを変更しなくても、ビルド設定を変更するだけで パフォーマンスを大きく変えることができます。Rust プログラムごとに、選択可能な ビルド設定は多数あります。選択した設定は、コンパイル時間、ランタイム速度、 メモリ使用量、バイナリサイズ、デバッグしやすさ、プロファイリングしやすさ、 コンパイル済みプログラムが実行されるアーキテクチャなど、コンパイルされたコードの いくつかの特性に影響します。
ほとんどの設定の選択は、1つ以上の特性を改善する一方で、 別の1つ以上の特性を悪化させます。たとえば、一般的なトレードオフは、 コンパイル時間が悪化することを受け入れる代わりに、ランタイム速度を高めることです。 プログラムにとって正しい選択は、あなたのニーズとプログラムの詳細に依存し、 パフォーマンス関連の選択(その大半がそうです)はベンチマークで 検証するべきです。
この章を注意深く読み、すべてのビルド設定の選択肢を理解する価値があります。
しかし、せっかちな人や忘れっぽい人のために、
cargo-wizard はこの情報をカプセル化しており、適切な
ビルド設定を選ぶ手助けをしてくれます。
Cargo は、ワークスペースのルートにある Cargo.toml ファイル内の
プロファイル設定だけを参照することに注意してください。依存関係で定義された
プロファイル設定は無視されます。したがって、これらのオプションは主に
ライブラリクレートではなく、バイナリクレートに関係します。
リリースビルド
最も重要なビルド設定の選択は単純ですが 見落としやすい ものです。高いパフォーマンスが必要なときは、リリースビルド を
開発ビルド ではなく使用していることを確認してください。
通常、これは Cargo に --release フラグを指定することで行います。
開発ビルドがデフォルトです。これはデバッグには適していますが、最適化されていません。
cargo build または cargo run を実行すると生成されます。(あるいは、
追加オプションなしで rustc を実行しても、最適化されていないビルドが生成されます。)
cargo build を実行したときの、次の最終出力行を考えてみましょう。
Finished dev [unoptimized + debuginfo] target(s) in 29.80s
この出力は、開発ビルドが生成されたことを示しています。コンパイルされたコードは
target/debug/ ディレクトリに配置されます。cargo run は開発ビルドを
実行します。
それに比べて、リリースビルドははるかに最適化されており、デバッグアサーションと
整数オーバーフローチェックを省き、デバッグ情報も省きます。開発ビルドに対して
10〜100倍の高速化は珍しくありません! cargo build --release または
cargo run --release を実行すると生成されます。(あるいは、rustc には
-O や -C opt-level など、最適化ビルド用の複数のオプションがあります。)
これは追加の最適化のため、通常は開発ビルドよりも時間がかかります。
cargo build --release を実行したときの、次の最終出力行を考えてみましょう。
Finished release [optimized] target(s) in 1m 01s
この出力は、リリースビルドが生成されたことを示しています。コンパイルされたコードは
target/release/ ディレクトリに配置されます。cargo run --release は
リリースビルドを実行します。
開発ビルド(dev プロファイルを使用)とリリースビルド(release プロファイルを使用)の
違いの詳細については、Cargo プロファイルのドキュメントを参照してください。
リリースビルドで使用されるデフォルトのビルド設定の選択は、コンパイル時間、 ランタイム速度、バイナリサイズなど、前述の特性の間で適切なバランスを提供します。 しかし、次のセクションで説明するように、調整可能な点は多数あります。
ランタイム速度の最大化
次のビルド設定オプションは、主にランタイム速度を最大化するように設計されています。 その一部は、バイナリサイズを削減する場合もあります。
コード生成ユニット
Rust コンパイラーは、コンパイルを並列化(したがって高速化)するために、クレートを
複数の コード生成ユニット に分割します。しかし、これにより、
潜在的な最適化の一部を見逃す可能性があります。コンパイル時間が増加する代わりに、
ユニット数を1に設定することで、ランタイム速度を向上させ、バイナリサイズを
削減できる場合があります。Cargo.toml ファイルに次の行を追加します。
[profile.release]
codegen-units = 1
リンク時最適化
リンク時最適化(LTO)は、ランタイム速度を10〜20%以上 向上させ、さらにバイナリサイズも削減できる、プログラム全体を対象とした 最適化手法です。その代償としてコンパイル時間は悪化します。LTO にはいくつかの 形態があります。
LTO の最初の形態は、軽量な LTO である thin local LTO です。
デフォルトでは、コンパイラーは0ではない最適化レベルを伴う任意のビルドで
これを使用します。これにはリリースビルドが含まれます。このレベルの
LTO を明示的に要求するには、Cargo.toml ファイルに次の行を入れます。
[profile.release]
lto = false
LTO の2つ目の形態は thin LTO です。これは少しより積極的で、
コンパイル時間も増加させる一方で、ランタイム速度を向上させ、
バイナリサイズを削減する可能性が高いものです。有効にするには、
Cargo.toml で lto = "thin" を使用します。
LTO の3つ目の形態は fat LTO です。これはさらに積極的で、
ビルド時間を再び増加させる一方で、パフォーマンスをさらに向上させ、
バイナリサイズをさらに削減する可能性があります(ただし 常にそうとは限りません)。
有効にするには、Cargo.toml で lto = "fat" を使用します。
最後に、LTO を完全に無効化することも可能です。これにより、ランタイム速度は
悪化し、バイナリサイズは増加する可能性が高いものの、コンパイル時間は短縮されます。
これには Cargo.toml で lto = "off" を使用します。これは
lto = false オプションとは異なることに注意してください。前述のとおり、
lto = false は thin local LTO を有効なままにします。
代替アロケーター
Rust プログラムで使用されるデフォルトの(システム)ヒープアロケーターを、 代替アロケーターに置き換えることが可能です。正確な効果は個々のプログラムと 選択した代替アロケーターに依存しますが、実際にはランタイム速度の大幅な向上と メモリ使用量の大幅な削減が確認されています。各プラットフォームの システムアロケーターにはそれぞれ固有の長所と短所があるため、効果は プラットフォームによっても異なります。代替アロケーターの使用は、 バイナリサイズとコンパイル時間を増加させる可能性も高いです。
jemalloc
LinuxとMacで人気のある代替アロケータの1つは jemalloc で、tikv-jemallocator クレート経由で使用できます。これを使用するには、Cargo.toml ファイルに依存関係を追加します。
[dependencies]
tikv-jemallocator = "0.5"
次に、Rustコードに以下を追加します。例えば src/main.rs の先頭に追加します。
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
さらにLinuxでは、jemallocを透過的巨大ページ(THP)を使用するように構成できます。これにより、メモリ使用量が増える可能性はありますが、プログラムをさらに高速化できる場合があります。
これは、プログラムをビルドする前に MALLOC_CONF 環境変数(または場合によっては _RJEM_MALLOC_CONF)を適切に設定することで行います。例:
MALLOC_CONF="thp:always,metadata_thp:always" cargo build --release
コンパイル済みプログラムを実行するシステムも、THPをサポートするように構成されている必要があります。詳細については、このブログ記事を参照してください。
mimalloc
多くのプラットフォームで動作するもう1つの代替アロケータは mimalloc で、mimalloc クレート経由で使用できます。これを使用するには、Cargo.toml ファイルに依存関係を追加します。
[dependencies]
mimalloc = "0.1"
次に、Rustコードに以下を追加します。例えば src/main.rs の先頭に追加します。
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
CPU固有命令
古い(または他の種類の)プロセッサーでのバイナリの互換性を気にしない場合は、x86-64 CPU向けのAVX SIMD命令など、特定のCPUアーキテクチャに固有の最新の(そして潜在的に最速の)命令を生成するようコンパイラに指示できます。
コマンドラインからこれらの命令を要求するには、-C target-cpu=native フラグを使用します。例:
RUSTFLAGS="-C target-cpu=native" cargo build --release
あるいは、1つ以上のプロジェクトについて、config.toml ファイルからこれらの命令を要求するには、以下の行を追加します。
[build]
rustflags = ["-C", "target-cpu=native"]
これにより、特にコンパイラがコード内にベクトル化の機会を見つけた場合、ランタイム速度が向上する可能性があります。
-C target-cpu=native が最適に機能しているかどうかわからない場合は、rustc --print cfg と rustc --print cfg -C target-cpu=native の出力を比較して、後者の場合にCPU機能が正しく検出されているかを確認してください。そうでない場合は、-C target-feature を使用して特定の機能をターゲットにできます。
プロファイルガイド最適化
プロファイルガイド最適化(PGO)は、プログラムをコンパイルし、プロファイリングデータを収集しながらサンプルデータ上で実行し、そのプロファイリングデータを使用してプログラムの2回目のコンパイルを導くコンパイルモデルです。これにより、ランタイム速度が10%以上向上する可能性があります。 例1, 例2。
これはセットアップに多少の労力を要する高度な手法ですが、場合によっては価値があります。詳細については、rustc PGOドキュメントを参照してください。また、cargo-pgo コマンドを使用すると、Rustバイナリの最適化にPGO(および類似の BOLT)を使いやすくなります。
残念ながら、PGOはcrates.ioでホストされ、cargo install 経由で配布されるバイナリではサポートされていないため、その有用性は制限されます。
バイナリサイズの最小化
以下のビルド構成オプションは、主にバイナリサイズを最小化するように設計されています。ランタイム速度への影響はさまざまです。
最適化レベル
Cargo.toml ファイルに以下の行を追加することで、バイナリサイズの最小化を目指す最適化レベルを要求できます。
[profile.release]
opt-level = "z"
これにより、ランタイム速度も低下する可能性があります。
代替案は opt-level = "s" で、これはバイナリサイズの最小化を少し控えめにターゲットにします。opt-level = "z" と比較すると、わずかに多いインライン化とループのベクトル化も許可します。
panic! 時にアボート
パニック時にアンワインドする必要がない場合、例えばプログラムが catch_unwind を使用していないためであれば、コンパイラに単にパニック時にアボートするよう指示できます。パニック時でも、プログラムはバックトレースを生成します。
これにより、バイナリサイズが小さくなり、ランタイム速度がわずかに向上する可能性があり、コンパイル時間もわずかに短縮される場合があります。Cargo.toml ファイルに以下の行を追加します。
[profile.release]
panic = "abort"
シンボルのストリップ
Cargo.toml に以下の行を追加することで、リリースビルドからシンボルをストリップするようコンパイラに指示できます。
[profile.release]
strip = "symbols"
例。
ただし、シンボルをストリップすると、コンパイル済みプログラムのデバッグやプロファイリングがより難しくなる可能性があります。例えば、ストリップされたプログラムがパニックした場合、生成されるバックトレースには通常より有用な情報が少なくなる可能性があります。正確な影響はプラットフォームによって異なります。
デバッグ情報をリリースビルドからストリップする必要はありません。デフォルトでは、ローカルのリリースビルドではデバッグ情報は生成されず、標準ライブラリのデバッグ情報はリリースビルドでRust 1.77以降自動的にストリップされています。
その他のアイデア
より高度なバイナリサイズ最小化手法については、優れた min-sized-rust リポジトリにある包括的なドキュメントを参照してください。
コンパイル時間の最小化
以下のビルド構成オプションは、主にコンパイル時間を最小化するように設計されています。
リンク
コンパイル時間の大部分は実際にはリンク時間であり、特に小さな変更後にプログラムを再ビルドする場合に顕著です。一部のプラットフォームでは、デフォルトのリンカーより高速なリンカーを選択できます。 1つの選択肢は lld です。これは Linux と Windows で利用できます。lld は Rust 1.90 以降、Linux でデフォルトのリンカーになっています。Windows ではまだデフォルトではありませんが、ほとんどのユースケースで動作するはずです。
コマンドラインから lld を指定するには、-C link-arg=-fuse-ld=lld フラグを使用します。例:
RUSTFLAGS="-C link-arg=-fuse-ld=lld" cargo build --release
または、1つ以上のプロジェクトに対して config.toml ファイルから lld を指定するには、次の行を追加します:
[build]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
lld の完全なサポートを追跡している GitHub Issue があります。
もう1つの選択肢は mold です。これは現在 Linux で利用できます。
上記の手順で lld を mold に置き換えるだけです。mold は多くの場合、lld より高速です。
例。
ただし、これはかなり新しいものでもあり、すべての場合で動作するとは限りません。
最後の選択肢は wild です。これは現在 Linux でのみ利用できます。mold よりさらに高速な可能性がありますが、成熟度は低くなります。
Mac では、システムリンカーが高速であるため、代替リンカーは必要ありません。
この章の他の選択肢とは異なり、別のリンカーを選択することにトレードオフはありません。通常とは異なることをしていない限り、リンカーはプログラムに対して正しく動作する可能性が高く、その場合、代替リンカーはデメリットなしに劇的に高速になることがあります。
デバッグ情報生成の無効化
release ビルドは最高のパフォーマンスを提供しますが、dev ビルドはより速くビルドできるため、開発中は多くの人が dev ビルドを使用します。dev ビルドを使用しているものの、デバッガーをあまり使わない場合は、debuginfo を無効にすることを検討してください。これにより、dev ビルド時間を大幅に改善でき、20〜40% ほど短縮できることがあります。 例。
デバッグ情報の生成を無効にするには、Cargo.toml ファイルに次の行を追加します:
[profile.dev]
debug = false
これは、スタックトレースに行情報が含まれなくなることを意味する点に注意してください。その行情報を保持したいが、デバッガー向けの完全な情報は必要ない場合は、代わりに debug = "line-tables-only" を使用できます。これでもコンパイル時間に関する利点の大半は得られます。
実験的な並列フロントエンド
nightly Rust を使用している場合は、実験的な並列フロントエンドを有効にできます。これはコンパイル時のメモリ使用量が増える代わりに、コンパイル時間を短縮する可能性があります。生成されるコードの品質には影響しません。
これは、たとえば次のように RUSTFLAGS に -Zthreads=N を追加することで行えます:
RUSTFLAGS="-Zthreads=8" cargo build --release
または、1つ以上のプロジェクトに対して config.toml ファイルから並列フロントエンドを有効にするには、次の行を追加します:
[build]
rustflags = ["-Z", "threads=8"]
8 以外の値も可能ですが、最良の結果をもたらす傾向があるのはその数です。
最良の場合、実験的な並列フロントエンドはコンパイル時間を最大 50% 短縮します。しかし効果には大きなばらつきがあり、コードの特性とそのビルド構成に依存します。また、一部のプログラムではコンパイル時間の改善がありません。
Cranelift コード生成バックエンド
nightly Rust を使用している場合、一部のプラットフォームで Cranelift コード生成バックエンドを有効にできます。これは生成されるコードの品質が低下する代わりに、コンパイル時間を短縮する可能性があります。そのため、release ビルドではなく dev ビルドに推奨されます。
まず、次の rustup コマンドでバックエンドをインストールします:
rustup component add rustc-codegen-cranelift-preview --toolchain nightly
コマンドラインから Cranelift を選択するには、-Zcodegen-backend=cranelift フラグを使用します。例:
RUSTFLAGS="-Zcodegen-backend=cranelift" cargo +nightly build
または、1つ以上のプロジェクトに対して config.toml ファイルから Cranelift を指定するには、次の行を追加します:
[unstable]
codegen-backend = true
[profile.dev]
codegen-backend = "cranelift"
詳細については、Cranelift ドキュメントを参照してください。
カスタムプロファイル
dev および release プロファイルに加えて、Cargo はカスタムプロファイルをサポートしています。たとえば、dev ビルドの実行速度が不十分で、release ビルドのコンパイル時間が日常的な開発には遅すぎると感じる場合、dev と release の中間にあるカスタムプロファイルを作成すると役立つかもしれません。
まとめ
ビルド構成に関しては、多くの選択を行う必要があります。以下のポイントは、上記の情報をいくつかの推奨事項としてまとめたものです。
- 実行時速度を最大化したい場合は、次のすべてを検討してください:
codegen-units = 1、lto = "fat"、代替アロケーター、およびpanic = "abort"。 - バイナリサイズを最小化したい場合は、
opt-level = "z"、codegen-units = 1、lto = "fat"、panic = "abort"、およびstrip = "symbols"を検討してください。 - どちらの場合でも、広範なアーキテクチャサポートが不要であれば
-C target-cpu=nativeを検討し、配布メカニズムで機能するのであればcargo-pgoを検討してください。 - それをサポートしているプラットフォームを使用している場合は、常により高速なリンカーを使用してください。そうすることにデメリットはないためです。
- これらの選択について追加の支援が必要な場合は、
cargo-wizardを使用してください。 - すべての変更を一度に1つずつベンチマークし、期待される効果があることを確認してください。
最後に、この issue は Rust コンパイラー自身のビルド構成の進化を追跡しています。Rust コンパイラーのビルドシステムは、ほとんどの Rust プログラムのものよりも特殊で複雑です。それでも、この issue はビルド構成の選択を大規模なプログラムにどのように適用できるかを示す点で有益かもしれません。