最適化:速度とサイズのトレードオフ
誰もがプログラムを超高速で超小さくしたいと願いますが、両方の特性を最大化することはできません。 このセクションは、rustcが提供する異なる最適化レベルについて、どのようにプログラムの実行時間とバイナリサイズに影響するかを説明します。
最適化なし
これはデフォルトです。cargo buildを実行する場合、開発(別名dev)プロファイルを使います。 このプロファイルはデバッグに最適化されています。そのため、デバッグ情報が有効化されており、最適化は一切有効化されていません。 つまり、-C opt-level = 0を使用します。
少なくともベアメタルの開発では、デバッグ情報はある意味ゼロコストです。 デバッグ情報は、Flash/ROMの容量を使いません。そのため、リリースプロファイルで、デフォルトで無効化されているデバッグ情報を有効化することをお勧めします。 これにより、リリースビルドをデバッグする時、ブレイクポイントを使うことができます。
[profile.release]
# シンボルは素晴らしく、Flashのサイズを増やしません
debug = true
最適化しないことはデバッグでは重要です。コードをステップ実行する時、プログラムをステートメントごとに実行しているように感じられるからです。 さらに、スタックの変数や関数の引数をGDBでprintすることができます。 コードが最適化されると、変数を表示しようとしても、$0 = <value optimized out>と表示されます。
devプロファイルの最大の欠点は、バイナリが大きく、遅いことです。 バイナリサイズは通常、より問題となります。最適化されていないバイナリは数十KiBもFlashを専有するからです。 ターゲットデバイスは、数十KiBものFlashを持っていないかもしれず、最適化されていないバイナリは、デバイス内に納まりません。
デバッガで扱いやすい、小さなバイナリを作ることができるのでしょうか?できます。良いやり方があります。
依存関係の最適化
nightlyでは、profile-overridesと呼ばれるCargoフィーチャがあります。これは、依存関係の最適化レベルをオーバーライドします。 このフィーチャを使って、全ての依存クレートのサイズを最適化しながら、トップクレートを最適化しないでデバッガで扱いやすくすることができます。。
Beware that generic code can sometimes be optimized alongside the crate where it is instantiated, rather than the crate where it is defined. If you create an instance of a generic struct in your application and find that it pulls in code with a large footprint, it may be that increasing the optimisation level of the relevant dependencies has no effect.
例を示します。
# Cargo.toml
[package]
name = "app"
# ..
[profile.dev.package."*"] # +
opt-level = "z" # +オーバーライドなしでは、次の通りです。
$ cargo size --bin app -- -A
app :
section size addr
.vector_table 1024 0x8000000
.text 9060 0x8000400
.rodata 1708 0x8002780
.data 0 0x20000000
.bss 4 0x20000000オーバーライドをすると、以下のようになります。
$ cargo size --bin app -- -A
app :
section size addr
.vector_table 1024 0x8000000
.text 3490 0x8000400
.rodata 1100 0x80011c0
.data 0 0x20000000
.bss 4 0x20000000トップクレートのデバッグ性を失うことなしに、Flash使用量を6KiB減らしています。 依存クレートの中に足を踏み入れると、<value optimized out>のメッセージを目にするでしょう。 しかし、依存クレートではなく、トップクレートをデバッグしたい場合がほとんどでしょう。 依存クレートをデバッグする必要がある場合、特定の依存クレートを最適化から除外するために、profile-overridesフィーチャを使えます。 例えば、以下のようになります。
# ..
# `cortex-m-rt`クレートは最適化しません
[profile.dev.package.cortex-m-rt] # +
opt-level = 0 # +
# しかし、他の依存クレートは最適化します
[profile.dev.package."*"]
codegen-units = 1 # better optimizations
opt-level = "z"
これで、トップクレートとcortex-m-rtクレートはデバッガで扱いやすくなります!
速度の最適化
2018-09-18のrustcは、3つの「速度最適化」を提供しています。opt-level = 1、2と3です。 cargo build --releaseを実行した時、デフォルトではopt-level = 3のリリースプロファイルを使います。
opt-level = 2と3は、バイナリサイズを犠牲にして、速度を最適化します。レベル3はレベル2より、ベクトル化とインライン化を行います。 特に、opt-levelが2以上の場合、LLVMがループを展開するのがわかるでしょう。 ループ展開は、Flash/ROMの観点からはよりコストが高いです(例えば、配列のループをゼロにする場合、26バイトから194バイトまで増加します)。 しかし、適切な条件では、実行時間が半分になります(例えば、イテレーションの回数が十分大きい場合)。
現在、opt-level = 2と3でループ展開を無効化する方法はありません。 ループ展開のコストを払うことができない場合、プログラムサイズの最適化をするべきです。
サイズの最適化
2018-09-18のrustcは、2つのサイズ最適化を提供しています。opt-level = "s"と"z"です。 これらの名前は、clang / LLVMから受け継いでおり、意味がわかりにくいです。 "z"は、"s"より小さなバイナリを作る意図を意味します。
リリースバイナリのサイズを最適化したい場合、Cargo.tomlのprofile.release.opt-level設定を下記の通り変更します。
[profile.release]
# または"z"
opt-level = "s"
これらの2つの最適化レベルは、LLVMのインラインしきい値を大幅に減らします。 インラインしきい値は、関数をインライン化するか否かを決めるために使われる基準値です。 Rustの原則の1つは、ゼロコスト抽象化です。 これらの抽象化は、不変条件を保持するため、多くの新しい型と小さな関数を使う傾向にあります (例えば、derefやas_refのような内部の値を借用するための関数)。 そのため、インラインしきい値を低くすると、LLVMが最適化の機会を失います (例えば、不要な分岐を削除したり、クロージャをインライン呼び出しにする、など)。
サイズの最適化を行っている時、バイナリサイズに影響があるかどうかを見るために、インラインしきい値を増やしたいかもしれません。 インラインしきい値を変更するお勧めの方法は、.cargo/config内のrustflagsに-C inline-thresholdフラグを追加することです。
# .cargo/config.toml
# cortex-m-quickstartテンプレートを使っていることを想定しています
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
# ..
"-C", "inline-threshold=123", # +
]
この値は何に使われるのでしょうか? 1.29.0では、下記の値は、異なる最適化レベルで使われるインラインしきい値です
opt-level = 3は275を使いますopt-level = 2は225を使いますopt-level = "s"は75を使いますopt-level = "z"は25を使います
サイズの最適化をするときは、225と275を試してみるべきです。