0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust と C 間の LTO(リンク時最適化)

Posted at

動機

Rust で C のライブラリを使うとき、最適化されているのか気になったので調べてみました。

TL;DR

環境

  • Windows11 24H2
  • WSL2
  • Debian 12
  • rustc 1.83.0

実装

Rust だけのコード

add_one 関数

まずの Rust だけのコード書きます。ビルドした成果物を objdump したときに見やすいように#![no_std]#![no_main]を付けています。

src/main.rs
#![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 にリンクします。

build.rs
fn main() {
    println!("cargo:rustc-link-lib=c");
}

Cargo.toml はこんな感じ。

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
add_one/src/lib.rs
#![no_std]

pub fn add_one(value: i32) -> i32 {
    value + 1
}

前述の Cargo.toml に add_one クレートを追加します。

Cargo.toml
[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 も書き換えます。

src/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 のコードを書きます。

src/add_one.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 から呼びます。

src/main.rs
#![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 をリンクします。

build.rs
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

やったぜ。

まとめ

よいお年をお迎えください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?