Rustのバイナリが大きい理由

κeenです。方々で言われる話ですがRustコンパイラの吐くバイナリはそこそこ大きいです。 この理由を自分で納得してなかったので追います。

様々な理由からnightlyを使います。

初期

$ cat hello.rs
fn main() {
    println!("Hello, World");
}
$ rustc +nightly hello.rs
$ ls -l hello
-rwxr-xr-x 1 kim kim 5049344 12月 18 23:30 allocator

5MBくらいあります。等価なCのコード(gccでオプションなし)が8.2Kだったのでかなり大きいですね

最適化

cargo --releaseと同じく-Copt-level=3を付けましょう。-Copt-level=sの方が小さくなりますが普段やらないので。

$ rustc +nightly -Copt-level=3  hello.rs
$ $ ls -l hello
-rwxr-xr-x 1 kim kim 5049200 12月 18 23:27 hello

ほんの少しだけ小さくなりました。

デバッグシンボル

stripしてませんね。デバッグシンボルを落とします。

$ rustc +nightly -Copt-level=3  hello.rs
$ strip hello
$ ls -l hello
-rwxr-xr-x 1 kim kim 461768 12月 18 23:35 hello

460KBくらいに減りました。 大体ここまでやったのがスタートラインですかね。 こっからもうちょいけずっていきます。

LTO

コンパイル時だけでなくリンク時にも最適化をします。

$ rustc +nightly -Copt-level=3 -Clto  hello.rs
$ strip hello
$ ls -l hello
-rwxr-xr-x 1 kim kim 453544 12月 18 23:37 hello

数KB減りました

アロケータ

Rustはlibcのmallocでなくjemallocを使っています。そこが効いてるかもしれません。

#![feature(alloc_system)]
extern crate alloc_system;

fn main() {
    println!("Hello, World");
}

このように書き換えて

$ rustc +nightly -Copt-level=3 -Clto  hello.rs
$ strip hello
$ ls -l hello
-rwxr-xr-x 1 kim kim 453544 12月 18 23:38 hello

あれ!?減ってない strings hello | grep jemalloc でまだjemallocのシンボルが残ってるようですし何か失敗してるのかもしれません。

原因がわからないので後回し。

panicをやめる

Rustはpanicしたときのために色々してます。panicしたら即座にabortするようにしてみましょう。

$ rustc +nightly -Copt-level=3 -Clto -Cpanic=abort hello.rs
$ strip hello
$ ls -l hello
-rwxr-xr-x 1 kim kim 433064 12月 18 23:41 hello

20KBくらい痩せました。

staticリンク

staticします

$ rustc +nightly -Copt-level=3 -Clto -Clink-args=-static -Cpanic=abort hello.rs
$ strip hello
$ ls -l hello
-rwxr-xr-x 1 kim kim 412504 12月 18 23:46 hello

もうちょい痩せました。

opt-level=s

ここまできたら最初は使わないと言っていたopt-level=sも試してみましょう

$ rustc +nightly -Copt-level=s -Clto -Clink-args=-static -Cpanic=abort hello.rs
$ strip hello
$ ls -l hello
-rwxr-xr-x 1 kim kim 408328 12月 18 23:49 hello

もう数KBばかり。

終わりに

Rustのバイナリサイズが(デバッグシンボルを除いても)大きいのはjemallocのせいと思ってたんですがシステムアロケータを使っても改善しませんでしたね。

因みに並行サポートを意識したRustは標準出力に吐くだけでも標準出力のロックを取って、と複雑なコードになっているので単純に見えるコードでもオーバーヘッドがありますね。

システムアロケータの件は納得いってないので追求したい。

参考

Why is a Rust executable large?

こちらではバイナリサイズが小さい上にjemallocをやめるとバイナリサイズが1/3くらいになってるのでやっぱり自分のやつは何かまちがってそう

2017-12-19 追記: システムアロケータの本気

古いドキュメントを参考にしていると指摘されました。 ちゃんと書いたらできました。

#![feature(alloc_system, allocator_api, global_allocator)]

extern crate alloc_system;


use alloc_system::System;

#[global_allocator]
static A: System = System;


fn main() {

    println!("Hello, World");

}
$ rustc +nightly -Copt-level=z -Clto -Clink-args=-static -Cpanic=abort hello.rs
$ strip hello
$ ls -l hello
-rwxr-xr-x 1 kim kim 145816 12月 19 10:06 hello

143KiB。Cと比べるとまだまだ大きいですが十分小さくなりました。

因みに「よくやる」コンパイルオプションだとこうなります。

$ rustc +nightly -Copt-level=3 hello.rs
$ ls -l hello
-rwxr-xr-x 1 kim kim 2587696 12月 19 10:10 hello
$ strip hello
$ ls -l hello
-rwxr-xr-x 1 kim kim 186960 12月 19 10:10 hello

元のstrip前の5MBやstrip後の460KBに比べて半分(以下)です。 jemallocで最低限のバイナリへのオーバーヘッドの半分を占めていたことになります。

jemalloclibcmallocに比べて速かったりRustに都合の良い(消費メモリが少なくて済む)アロケーションをしてくれたりするので使われているのですがバイナリサイズを気にするならシステムアロケータを使うのも手でしょう。

jemalloclibcmallocのベンチマークは読者の課題とする。

Written by κeen
Older article
Rust & Swagger