Rust製ツールのリリースバイナリを無理なくコンパクトにする
シンプルな機能のRustプログラムを書いていたら、リリースビルドでも420KB超と予想以上にサイズが大きくなった。バイナリサイズの削減がどの程度現実的なのかをざっと検証してみる*1
初期状態の確認
releaseビルドの初期状態で429104 bytes
cargo bloat
で中身を確認すると、大半が標準ライブラリ由来、特にpanic時のハンドラから呼ばれていると思しきbacktrace生成用のライブラリが上位を占める
$ cargo bloat --release -n 10 ... File .text Size Crate Name 0.9% 6.3% 14.7KiB std std::sys::backtrace::_print_fmt:: 0.7% 5.2% 12.1KiB std std::backtrace_rs::symbolize::gimli::Context::new 0.6% 4.0% 9.4KiB std gimli::read::dwarf::Unit<R>::new 0.6% 4.0% 9.3KiB std miniz_oxide::inflate::core::decompress 0.4% 2.9% 6.9KiB std addr2line::ResUnit<R>::find_function_or_location:: ...
バイナリサイズの削減
Cargo.tomlへの定番設定
Cargo.tomlに以下を追加する
[profile.release] # opt-level = "z" lto = true codegen-units = 1 strip = true panic = "abort"
panic = "abort"
がキー!という書き方をしているblog記事類も見つかるが、ここへの期待度はさほど高くない。よほどきれいにstripされない限りは周辺ライブラリの容量削減は見込めないため
これらの設定を追加後にリビルドして326040 bytes、最初の429104 bytesと比較すると24%の削減
opt-level = "z"
指定を有効化するとさらに9KBほど減るが、速度を犠牲にする判断をしたくないケースも多い気はするので今回は最終的にコメントアウトした。典型的には短めの固定長配列に対するループのアンロール処理を無効化することでサイズを削るのだろう
stdのコンパクト版ビルド
2018年頃には確立していた手法として、stdの実行時用コンパクト版をビルドする方法がある*3。MSVCでいうところのVC++ Runtime debug版とrelease版のようなもので、Rustでは通常debug時でもrelease時でも同一のプリビルトstdライブラリを静的リンクしているが、ビルド時にソースからstdを構築してバンドルする仕組み(概念としてはstd Aware Cargoと表現されている*2)によって不要なものを削ってバイナリサイズを削れる。当初はXargoで実現されていた機能だが、現在はnightly版のbuild-std
を使用する
$ cargo +nightly build -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --release
ここで指定している-Z
オプション群はnightly版のCargoでしかまだ使えない(2024年12月時点)ので、rustupから予めnightly版のtoolchainとstdビルド用のrust-srcを導入しておく必要がある
rustup toolchain install nightly rustup component add rust-src --toolchain nightly
build-std-features=panic_immediate_abort
指定でCargo.toml側のpanic = "abort"
指定を補完しているのは良い
これで26KBまで劇的に削減できる
なお、stdをビルドする都合上リリースビルドの所要時間は伸びる。手元の古いPCでは30秒程度のオーバーヘッドが発生するが、リリースビルドをそう頻繁にやるわけではないので開発体験を悪化させる性質のものではない。配布バイナリのサイズ最適化という文脈では許容できる範囲と考えられる
ただ、build-stdは前述のように2024年時点でもnightlyの機能である。std-aware Cargoの仕様策定には複雑な要件が絡み、安定化のためのディスカッションポイントはどれもパワーが必要そうで、すでに数年間議論が止まっていたり当初の議論リード者の関与が薄くなっている点も複数ある。バイナリサイズ削減がビジネス価値につながる企業が急にスポンサーとして付く、のような大幅加速イベントが発生しない限り、2030年ぐらいまでnightlyのままであっても驚きはない状況に見える
build-std
はまだpreliminaryでexperimentalな存在という扱いであることには注意が必要。開発段階で各種自動テストを通していても言語ランタイム/標準ライブラリが差し替えられた状態でそれらの結果が信用できるか、というのは現状での各種リリースバイナリをbuild-std版のstdライブラリと静的リンクして出力することを躊躇するに十分な理由で、そう考えるとよほど特殊な事情がない限りはnightly範囲外のCargo.toml設定群で満足しておくのが無難ではある
実行環境に応じた検討事項
配布サイズと実行時のトレードオフ
配布時のサイズをさらに小さくする選択肢としてはUPXによる実行可能ファイルの圧縮*4やlzipなどの一般的な圧縮形式の利用が考えられる。UPXは実行時にメモリ上でバイナリを展開する必要があり、実際の配布サイズとしては一般的な圧縮形式と大きな差は出ないため、見かけ上の容量削減(自己満足とも言える)に留まる可能性がそれなりに高い。たしかに今回のターゲットでは26KB→15KBぐらいに縮みはしたが作っているツールの性質的には特にUPXは不要だと感じた
WebAssembly向けの考慮
wasm環境での実行を想定する場合、stdをカスタムビルドするよりもno_std
を選択するほうが現実的なケースも多そう。しかしstdの要素は案外あちこちで必要になるのでその都度代替実装を探したり書いたりとしていくハードルは結構高い*5
[*1] https://github.com/johnthagen/min-sized-rust というチュートリアルが丁寧に各手法を紹介しているので参考になる。結果としてはこの内容を辿っていった
[*3] Xargoは2018年以降アクティブな開発は行われていない。 https://github.com/japaric/xargo/issues/193
[*4] Ultimate Packer for eXecutables https://github.com/upx/upx
[*5] wasm向けのno-stdについてはhttps://hackmd.io/@alxiong/rust-no-stdが出発点としてよさそう