動機
Rust で C のライブラリを使うとき、最適化されているのか気になったので調べてみました。
TL;DR
環境
- Windows11 24H2
- WSL2
- Debian 12
- rustc 1.83.0
実装
Rust だけのコード
add_one 関数
まずの Rust だけのコード書きます。ビルドした成果物を objdump したときに見やすいように#![no_std]
、#![no_main]
を付けています。
#![no_std]
#![no_main]
fn add_one(value: i32) -> i32 {
value + 1
}
#[no_mangle]
fn main() -> i32 {
add_one(41)
}
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
ビルドできるように libc にリンクします。
fn main() {
println!("cargo:rustc-link-lib=c");
}
Cargo.toml はこんな感じ。
[package]
name = "mytest"
version = "0.1.0"
edition = "2021"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
実行。
$ cargo run --release
Compiling mytest v0.1.0 (/home/benki/rust/mytest)
Finished `release` profile [optimized] target(s) in 0.35s
Running `target/release/mytest`
$ echo $?
42
はい、ちゃんと動いてます。objdump して main 関数を確認します。
$ objdump -d target/release/mytest
(中略)
0000000000001130 <main>:
1130: b8 2a 00 00 00 mov $0x2a,%eax
1135: c3 ret
最適化によって add_one 関数は消え去ってしまって、eax レジスタに 42 が代入されて、そのままリターンしています。
add_one クレート
次は add_one 関数を別クレートにしてみます。最適化されるでしょうか?
$ cargo new add_one --lib
#![no_std]
pub fn add_one(value: i32) -> i32 {
value + 1
}
前述の Cargo.toml に add_one クレートを追加します。
[package]
name = "mytest"
version = "0.1.0"
edition = "2021"
+ [dependencies]
+ add_one = { path = "../add_one" }
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
main.rs も書き換えます。
#![no_std]
#![no_main]
- fn add_one(value: i32) -> i32 {
- value + 1
- }
+ use add_one::add_one;
#[no_mangle]
fn main() -> i32 {
add_one(41)
}
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
別クレートにしてもばっちり最適化されています。(そりゃそうだ)
$ cargo run --release
Compiling mytest v0.1.0 (/home/benki/rust/mytest)
Finished `release` profile [optimized] target(s) in 0.42s
Running `target/release/mytest`
$ echo $?
42
$ objdump -d target/release/mytest
(中略)
0000000000001130 <main>:
1130: b8 2a 00 00 00 mov $0x2a,%eax
1135: c3 ret
Rust と C の混在
次に C の関数を呼ぶように変更します。まず C のコードを書きます。
int add_one(int value) {
return value + 1;
}
コンパイルしてライブラリにします。
$ cc -c src/add_one.c -o add_one.o -O2
$ ar -r libadd_one.a add_one.o
この C ライブラリの関数を Rust から呼びます。
#![no_std]
#![no_main]
+ extern "C" {
+ fn add_one(value: i32) -> i32;
+ }
#[no_mangle]
fn main() -> i32 {
+ unsafe { add_one(41) }
}
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
libadd_one.a をリンクします。
fn main() {
+ // リンクするライブラリのある場所をリンカに教える
+ println!("cargo:rustc-link-search=.");
+ // リンクするライブラリをリンカに教える
+ println!("cargo:rustc-link-lib=add_one");
println!("cargo:rustc-link-lib=c");
}
予想通りですが最適化されません。(add_one 関数内で add 命令じゃなくて lea 命令が使われるのですね。おもしろいなぁ)
$ cargo run --release
Compiling mytest v0.1.0 (/home/benki/rust/mytest)
Finished `release` profile [optimized] target(s) in 0.36s
Running `target/release/mytest`
$ echo $?
42
$ objdump -d target/release/mytest
(中略)
0000000000001130 <main>:
1130: bf 29 00 00 00 mov $0x29,%edi
1135: ff 25 95 2e 00 00 jmp *0x2e95(%rip)
113b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
0000000000001140 <add_one>:
1140: 8d 47 01 lea 0x1(%rdi),%eax
1143: c3 ret
gcc ではダメっぽいので clang を使います。Debian 12 でsudo apt install clang
したらバージョン 14 が入りました。clang でコンパイルしてライブラリにします。
$ clang -flto=thin -c src/add_one.c -o add_one.o -O2
$ llvm-ar -r libadd_one.a add_one.o
rustc に C のコードも LTO するように伝えます。
$ RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" cargo run --release
コンパイルエラーが出ました。Rust 1.83.0 は LLVM 19 を使っていて、clang が LLVM 14 なので中間コードに互換性がないって感じでしょうか?
Compiling mytest v0.1.0 (/home/benki/rust/mytest)
error: linking with `clang` failed: exit status: 1
|
(中略)
= note: ld.lld: error:(略)Opaque pointers are only supported in -opaque-pointers mode (Producer: 'LLVM19.1.1-rust-1.83.0-stable' Reader: 'LLVM 14.0.0')
clang: error: linker command failed with exit code 1 (use -v to see invocation)
error: could not compile `mytest` (build script) due to 1 previous error
仕方がないので LLVM 19 をインストールします。(ストレージを 8.2G 消費します。)
$ curl -LO https://github.com/llvm/llvm-project/releases/download/llvmorg-19.1.0/LLVM-19.1.0-Linux-X64.tar.xz
$ tar -xJvf LLVM-19.1.0-Linux-X64.tar.xz
LLVM-19.1.0-Linux-X64/bin
にパスを通して clang 19 で C のコードをコンパイルします。
$ clang -flto=thin -c src/add_one.c -o add_one.o -O2
$ llvm-ar -r libadd_one.a add_one.o
今度はビルドに成功しました。
$ RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" cargo run --release
Compiling mytest v0.1.0 (/home/benki/rust/mytest)
Finished `release` profile [optimized] target(s) in 1.79s
Running `target/release/mytest`
$ echo $?
42
最適化されているか確認します。
$ objdump -d target/release/mytest
(中略)
0000000000001690 <main>:
1690: b8 2a 00 00 00 mov $0x2a,%eax
1695: c3 ret
やったぜ。
まとめ
よいお年をお迎えください。