### なぜ Rust からアセンブリを呼び出すと、C からの呼び出しよりも遅くなることがあるか

手書きで作成したアセンブリルーチンを **C** から呼び出す場合、コンパイラは次のように最適化できます。

1. **プレーンで保護されていない呼び出しを生成** – 関数には Rust のランタイムオーバーヘッドがありません。  
2. **呼び出し元のレジスタを保持** – 呼び出し規約(例:`__cdecl` や `__stdcall`)に従い、保護すべきレジスタと破壊されるレジスタを区別します。  
3. **安全性チェックを行わない** – C はデフォルトで「unsafe」とみなすため、追加の検証が発生しません。

一方で同じアセンブリルーチンを **Rust** から呼び出すと、以下のような微妙な差異がオーバーヘッドを招くことがあります。

| 観点 | Rust 呼び出し | C 呼び出し |
|------|----------------|------------|
| **呼び出し規約** | デフォルトでは `extern "C"` がシステム ABI と一致しますが、`#[no_mangle]` を明示せずに使用すると、コンパイラが名前マングリングやシンボル解決を処理する小さなラッパーを追加することがあります。 | 直接 ABI を使用し、ラッパーは不要です。 |
| **ABI の不一致** | Rust の関数シグネチャがアセンブリの期待するパラメータ(呼び出し規約・スタック整列・レジスタ使用)と合わない場合、コンパイラはスタック/整列を調整したり、引数を別順序で渡すコードを挿入します。 | C コンパイラは呼び出しのレイアウトを正確に知っています。 |
| **安全性ラッパー** | Rust は `extern "C"` 境界を跨ぐ際に追加の安全チェック(例:NULL ポインタの検査、関数ポインタが NULL でないことの確認)を生成する場合があります。これらは微小ですがマイクロベンチマークでは影響します。 | C にはそのようなチェックはありません。 |
| **インライン化と最適化** | アセンブリルーチン自体はインライン化できないため、Rust は通常の呼び出し命令を発行します。周囲の Rust コードが高度に最適化(例:ヘルパー関数の積極的なインライン化)されていると、呼び出しオーバーヘッドが目立ちます。 | C コンパイラは小さなラッパーをインライン化したり、一部チェックを削除してオーバーヘッドを減らすことがあります。 |
| **リンカ / シンボル解決** | Rust のリンカーは、`#[link(name = "...")]` で正しく宣言されていない `extern` シンボルに対し追加のリロケーションエントリを生成する場合があり、一部プラットフォームでは実行時に微小な検索コストが発生します。 | C リンカーは通常、余分な間接参照なしでシンボルを直接解決します。 |

#### 測定可能な遅延の典型的原因

1. **呼び出し規約の不一致** – 例:Rust が `extern "C"` を使用しているが、アセンブリは `__stdcall`(またはその逆)を期待している。  
2. **スタック整列のズレ** – アセンブリルーチンが 16 バイト整列を前提としているのに、Rust の呼び出し側で 8 バイトのまま残っていると、コンパイラはスタック調整コードを挿入します。  
3. **名前マングリング / シンボル検索** – `#[no_mangle]` を忘れると追加の名前解決ステップが発生し、別関数が呼び出される可能性があります。  
4. **引数渡しの違い** – Rust がレジスタで引数を渡す一方、アセンブリはスタックを期待している(または逆)と、余分な移動命令が生成されます。

#### それらを比較可能にする方法

```rust
#[no_mangle]
pub extern "C" fn my_asm_fn(arg1: i32, arg2: i64) -> i32 {
    // C の呼び出し規約に従ったインラインアセンブリ。
    unsafe { asm!("...", in("rdi") arg1, in("rsi") arg2, out("eax") ret); }
}
```

- **`extern "C"` を明示的に宣言** して、Rust が C コードと同じ ABI を使用するようにします。  
- **`#[no_mangle]` を使う** ことで名前マングリングを防ぎます。  
- **引数の順序・型を正確に合わせる** ことでアセンブリ側の期待と一致させます。  
- **スタック整列を保証する**(必要なら `#[inline(always)]` を付けたり、手動で整列処理を入れたりします)。

これらが揃えば、Rust の呼び出しオーバーヘッドはほぼ C と同等になり、ベンチマーク上で差異が見えなくなるはずです。もしまだ違いが残る場合は、生成されたアセンブリをプロファイルして、余分な前処理/後処理コードが出力されていないか確認してください。

2025/12/27 23:13

### なぜ Rust からアセンブリを呼び出すと、C からの呼び出しよりも遅くなることがあるか 手書きで作成したアセンブリルーチンを **C** から呼び出す場合、コンパイラは次のように最適化できます。 1. **プレーンで保護されていない呼び出しを生成** – 関数には Rust のランタイムオーバーヘッドがありません。 2. **呼び出し元のレジスタを保持** – 呼び出し規約(例:`__cdecl` や `__stdcall`)に従い、保護すべきレジスタと破壊されるレジスタを区別します。 3. **安全性チェックを行わない** – C はデフォルトで「unsafe」とみなすため、追加の検証が発生しません。 一方で同じアセンブリルーチンを **Rust** から呼び出すと、以下のような微妙な差異がオーバーヘッドを招くことがあります。 | 観点 | Rust 呼び出し | C 呼び出し | |------|----------------|------------| | **呼び出し規約** | デフォルトでは `extern "C"` がシステム ABI と一致しますが、`#[no_mangle]` を明示せずに使用すると、コンパイラが名前マングリングやシンボル解決を処理する小さなラッパーを追加することがあります。 | 直接 ABI を使用し、ラッパーは不要です。 | | **ABI の不一致** | Rust の関数シグネチャがアセンブリの期待するパラメータ(呼び出し規約・スタック整列・レジスタ使用)と合わない場合、コンパイラはスタック/整列を調整したり、引数を別順序で渡すコードを挿入します。 | C コンパイラは呼び出しのレイアウトを正確に知っています。 | | **安全性ラッパー** | Rust は `extern "C"` 境界を跨ぐ際に追加の安全チェック(例:NULL ポインタの検査、関数ポインタが NULL でないことの確認)を生成する場合があります。これらは微小ですがマイクロベンチマークでは影響します。 | C にはそのようなチェックはありません。 | | **インライン化と最適化** | アセンブリルーチン自体はインライン化できないため、Rust は通常の呼び出し命令を発行します。周囲の Rust コードが高度に最適化(例:ヘルパー関数の積極的なインライン化)されていると、呼び出しオーバーヘッドが目立ちます。 | C コンパイラは小さなラッパーをインライン化したり、一部チェックを削除してオーバーヘッドを減らすことがあります。 | | **リンカ / シンボル解決** | Rust のリンカーは、`#[link(name = "...")]` で正しく宣言されていない `extern` シンボルに対し追加のリロケーションエントリを生成する場合があり、一部プラットフォームでは実行時に微小な検索コストが発生します。 | C リンカーは通常、余分な間接参照なしでシンボルを直接解決します。 | #### 測定可能な遅延の典型的原因 1. **呼び出し規約の不一致** – 例:Rust が `extern "C"` を使用しているが、アセンブリは `__stdcall`(またはその逆)を期待している。 2. **スタック整列のズレ** – アセンブリルーチンが 16 バイト整列を前提としているのに、Rust の呼び出し側で 8 バイトのまま残っていると、コンパイラはスタック調整コードを挿入します。 3. **名前マングリング / シンボル検索** – `#[no_mangle]` を忘れると追加の名前解決ステップが発生し、別関数が呼び出される可能性があります。 4. **引数渡しの違い** – Rust がレジスタで引数を渡す一方、アセンブリはスタックを期待している(または逆)と、余分な移動命令が生成されます。 #### それらを比較可能にする方法 ```rust #[no_mangle] pub extern "C" fn my_asm_fn(arg1: i32, arg2: i64) -> i32 { // C の呼び出し規約に従ったインラインアセンブリ。 unsafe { asm!("...", in("rdi") arg1, in("rsi") arg2, out("eax") ret); } } ``` - **`extern "C"` を明示的に宣言** して、Rust が C コードと同じ ABI を使用するようにします。 - **`#[no_mangle]` を使う** ことで名前マングリングを防ぎます。 - **引数の順序・型を正確に合わせる** ことでアセンブリ側の期待と一致させます。 - **スタック整列を保証する**(必要なら `#[inline(always)]` を付けたり、手動で整列処理を入れたりします)。 これらが揃えば、Rust の呼び出しオーバーヘッドはほぼ C と同等になり、ベンチマーク上で差異が見えなくなるはずです。もしまだ違いが残る場合は、生成されたアセンブリをプロファイルして、余分な前処理/後処理コードが出力されていないか確認してください。

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

著者らは、rav1d の CDEF フィルタが dav1d の同等機能より約 30 % 遅く、総デコード時間の約 0.5 % を占めていることを発見しました。プロファイリングによりボトルネックは単一の

ld1
命令にあり、期待される 10 サンプルではなく 441 サンプルがロードされていたことが判明しました。この問題は、生のポインタと入れ子になった
FFISafe<WithOffset>
ラッパーをアセンブリ関数へ渡すことで発生し、コンパイラがスタックレイアウトを最適化できず、必要以上に約 144 バイト多くのスタック割り当てが行われました。

未使用の

FFISafe
引数(
_dst
_top
_bottom
)を削除し、
cdef_filter_block_c_erased
をスタブ化することで、その関数のサンプルカウントは 1,562 から約 1,260 に減少しました。
WithOffset<T>
#[repr(C)]
を追加し、シグネチャを入れ子ラッパーではなく
WithOffset<*const FFISafe<...>>
を使用するよう再構成すると、コンパイラは冗長なスタックスロットを排除できました。
cargo‑asm
の比較で
rav1d_cdef_brow
における割り当てが減少し、プロファイルでは dav1d と比べた場合の性能差が約 5 % に留まることが報告されました。

これらの変更後も純粋な Rust のフォールバックは正しく機能し続け、リファクタリングにより ABI‑安全ラッパーの不一致が低レベル Rust コードでスタックレイアウトの非効率性とキャッシュストールを引き起こすことが示されました。

本文

1 %高速化を目指した rav1d のフォローアップ

前回は、Rust 実装の rav1d と C ベースラインの dav1d を比較し、Rust 側が遅いと判明した関数を特定しました。
本日はその結果に対する「小さな返済」を行います。両プロジェクトは手書きアセンブリ関数を共有しているため、その関数を基準に実装差分を追跡します――理想的には完全一致です。実際、ほぼ全てが一致しました。


概要

本日は「なぜ」三つの疑問に答えます。

なぜ説明
1. 特定アセンブリ関数が Rust 側で遅いRust でデータをロードする手順が遅く、
samply
の asm ビューで確認できます。
2. ロードが遅い理由LLVM IR を調べると、Rust はスタック上に余分なデータを保存しています。
3. なぜ余分なスタック?コンパイラは関数ポインタを通じた特定の Rust 抽象化を除去できません!

対策としてコンパイラフレンドリーな実装へ切り替え(PR)しました。

備考
すべてのベンチマークは MacBook 上で行っています。ツールが限定されるため推測も入ります。詳細を知っている方はコメントしてください――macOS のプロファイリング記事を書いていただければ幸いです 🍎💨。


ベンチマークの再実行

# rav1d を特定コミットに戻してビルド
./rav1d $ git checkout cfd3f59 && cargo build --release

# dav1d のプロファイルを取得
./rav1d $ sudo samply record ./target/release/dav1d -q -i Chimera-AV1-8bit-1920x1080-6736kbps.ivf -o /dev/null --threads 1

samply
の「逆呼び出しスタック」ビューに切り替え、
cdef_
関数をフィルタリングして neon サフィックスを比較します。左側が dav1d(C)、右側が rav1d(Rust)です。ほとんどの関数は約10 %以内で一致していますが、
cdef_filter4_pri_edged_8bpc_neon
は30 %遅くなっています(350サンプル ≈ 0.35 s、総実行時間の約0.5 %)。


命令を調べる

samply
の asm ビューで差異は単一命令に集約されます。

ld1 {v0.s}[2], [x13]   ; 10サンプル (C) vs 441サンプル (Rust)

ld1
は32ビット要素を SIMD レジスタ
v0
のレーン 2 にロードします。周辺コードは以下の通りです。

add x12, x2, #0x8
add x13, x2, #0x10
add x14, x2, #0x18
ld1 {v0.s}[0], [x2]
ld1 {v0.s}[1], [x12]
ld1 {v0.s}[2], [x13]   ; ← 遅い
ld1 {v0.s}[3], [x14]

x2
は関数に渡されたポインタ
tmp
を保持しています。

unsafe extern "C" fn filter(
    dst: *mut DynPixel,          // x0
    dst_stride: ptrdiff_t,       // x1
    tmp: *const MaybeUninit<u16>,// x2
    …
) -> ()

このアセンブリ関数は

cdef_filter_neon_erased
から呼び出され、未初期化の
u16
バッファ
tmp_buf
をスタックに確保し、asm パディングルーチンで埋めます。
なぜ連続したバッファからロードする方が遅いのでしょうか?


「未使用引数を削除してみた」修正

関数シグネチャには三つの未使用引数があります。

unsafe extern "C" fn cdef_filter_neon_erased<BD: BitDepth, ..>(
    dst: *mut DynPixel,
    stride: ptrdiff_t,
    left: *const [LeftPixelRow2px<DynPixel>; 8],
    top: *const DynPixel,
    bottom: *const DynPixel,
    …,
    _dst: *const FFISafe<Rav1dPictureDataComponentOffset>,
    _top: *const FFISafe<CdefTop>,
    _bottom: *const FFISafe<CdefBottom>,
)

これらを削除(純粋 Rust のフォールバックはスタブで置き換え)すると、

cdef_filter4_pri_edged_8bpc_neon
1 562 から 1 268 サンプルに減少します(dav1d の 1 199 に5 %以内)。さらに
ld1
命令の実行時間も dav1d と同等になります。


背後で何が起きたか?

cargo asm
を使ってスタック確保を確認すると、ベースライン版は高速化版より約144バイト多く確保しています。

baseline: alloca [16 x i8]  ×4
         alloca [24 x i8]  ×2
         …
faster : alloca [16 x i8]
         alloca [24 x i8]

余分な割り当ては、削除した

dst
top
bottom
の複数インスタンスに対応します。


なぜ未使用引数を削除すると改善するのか?

WithOffset
の役割

#[derive(Clone, Copy)]
pub struct WithOffset<T> {
    pub data: T,
    pub offset: usize,
}

64bit 系ではサイズは

size_of::<T>() + 8
です。
FFI に安全でないため、Rust は
FFISafe
でラップします。

let top_ptr: *mut DynPixel = top.as_ptr::<BD>().cast();
...
let top = FFISafe::new(&top);

コンパイラはラップされた値への生ポインタしか見えず、最適化が制限されます。未使用引数を削除したことでスタックレイアウトが変わり、問題の

ld1
が停止しなくなりました。

WithOffset
を FFI‑safe にする

#[repr(C)]
を付けるとメモリレイアウトが予測可能になります。

#[derive(Clone, Copy)]
#[repr(C)]
pub struct WithOffset<T> {
    pub data: T,
    pub offset: usize,
}

これにより、FFI 境界で「分解済み」バージョンを渡せます。

top: WithOffset<&'a u8>,          // *const FFISafe<WithOffset<&'a u8>> の代わり

関数内ではラッパーを再構築します。

let top = WithOffset {
    data: FFISafe::new(&top.data),
    offset: top.offset,
};

これにより余計な間接参照がなくなり、コンパイラは各引数が一度だけ使用されることを把握でき、スタック負荷が軽減します。


最終結果

repr(C)
の変更と未使用引数の削除後:

  • cdef_filter4_pri_edged_8bpc_neon
    サンプル数: 1 260(dav1d から5 %以内)
  • 全ての
    ld1
    ロード命令が dav1d のパフォーマンスと一致
  • 純 Rust フォールバックも正しく動作

テイクアウェイ

  • 未使用パラメータはスタック使用量を増やし、性能に影響します。
  • FFISafe
    のようなラッパーは間接参照を導入し、最適化を妨げます。
  • 構造体を
    #[repr(C)]
    にして FFI 境界で直接渡すと、コンパイラの最適化が再び働き、実行速度が向上します。

ぜひ変更点を試してみてください。macOS のプロファイリングに関する記事を書いていただけると嬉しいです!

同じ日のほかのニュース

一覧に戻る →

2025/12/30 6:46

USPS(米国郵便公社)が切手印日付システムの変更を発表しました。

## Japanese Translation: > **概要:** > USPSは最終規則(FR Doc. 2025‑20740)を発行し、国内郵便マニュアルに「セクション 608.11 —『切手印と郵便保有』」を追加しました。この規則では、切手印の定義が正式に示され、該当する印記がリストアップされています。切手印は印付け日でUSPSがその物件を保有していることを確認しますが、必ずしもアイテムの最初の受理日と同一ではありません。USPSは通常業務で全ての郵便に切手印を貼らないため、切手印が欠落していても、その物件が未処理だったとは限りません。機械による自動切手印は、施設内で最初に行われた自動処理操作の日付(「date of the first automated processing operation」)を表示し、投函日ではなく、地域輸送最適化(RTO)や路線ベースのサービス基準により受理日より遅くなることがあります。切手印は小売ユニットからの輸送後やカレンダー日がまたがる場合に付けられることが多いため、郵送日を示す信頼できる指標ではありません。同一日の切手印を確保するには、小売窓口で手動(ローカル)切手印を依頼できます。小売窓口で料金を支払うと「Postage Validation Imprint(PVI)」が付与され、受理日が記録されます。また、郵便証明書、登録メール、または認定メールは提示日を裏付ける領収書として機能します。この規則の影響は税務申告において重要です。IRC §7502 は、文書が期限までに物理的に届けられなかった場合に、提出の適時性を判断する際に切手印の日付を使用しています。

2025/12/30 1:07

**Zig における静的割り当て** Zig のコンパイル時メモリ管理を使えば、実行時ではなくコンパイル時にストレージを確保できます。データ構造のサイズが事前に分かっている場合やヒープ割り当てを避けたいときに便利です。 ### 重要概念 - **コンパイル時定数** `const` や `comptime` の値を使い、コンパイラがコンパイル中に評価できるサイズを記述します。 - **固定長配列** リテラルサイズで配列を宣言します。 ```zig const buf = [_]u8{0} ** 128; // 128 バイト、すべてゼロ初期化 ``` - **静的フィールドを持つ構造体** 固定長配列やその他コンパイル時に決まる型を含む構造体を定義します。 ### 例 ```zig const std = @import("std"); // 静的サイズのバッファを持つ構造体 pub const Message = struct { id: u32, payload: [256]u8, // 256 バイト、コンパイル時に確保 }; // 静的割り当てを使う関数 fn process(msg: *Message) void { // ヒープ割り当ては不要;msg はスタック上またはグローバルに存在 std.debug.print("ID: {d}\n", .{msg.id}); } pub fn main() !void { var msg = Message{ .id = 42, .payload = [_]u8{0} ** 256, // すべてのバイトをゼロで初期化 }; process(&msg); } ``` ### 利点 - **決定的なメモリ使用量** – サイズはコンパイル時に分かる - **実行時割り当てオーバーヘッドがゼロ** – ヒープアロケータ呼び出しなし - **安全性** – コンパイラが境界と寿命を検証できる ### 使うべき場面 - 固定長バッファ(例:ネットワークパケット、ファイルヘッダー) - 短時間しか存続しない小規模補助データ構造 - 性能や決定的な動作が重要な状況 --- コンパイル時定数・固定配列・構造体定義を活用することで、Zig は最小限のボイラープレートで最大の安全性を保ちつつメモリを静的に割り当てることができます。

## Japanese Translation: > **概要:** > このプロジェクトは、Zigで書かれた軽量Redis互換のキー/バリューサーバー「kv」を構築し、最小限のコマンドセットで本番環境に適した設計を目指しています。コアデザインでは起動時にすべてのメモリを確保することで、実行中にダイナミックヒープを使用せず、レイテンシスパイクやユース・アフター・フリー(use‑after‑free)バグを回避します。接続は`io_uring`で非同期に処理され、システムは3つのプール(Connection、受信バッファプール、送信バッファプール)を事前確保し、デフォルトでは約1000件までの同時接続数をサポートします。各接続は設定パラメータから派生した固定サイズの受信/送信バッファを使用します。 > コマンド解析はRedisのRESPプロトコルのサブセットに従い、Zigの`std.heap.FixedBufferAllocator`を用いてゼロコピーで解析し、各リクエスト後にアロケータをリセットします。バッファサイズは`list_length_max`と`val_size_max`に依存します。 > ストレージは未管理型の`StringHashMapUnmanaged(Value)`を使用し、初期化時に`ensureTotalCapacity`で容量を確保します。キーと値は共有`ByteArrayPool`に格納され、マップはポインタのみを保持します。削除操作では墓石(tombstone)が残り、墓石数が増えると再ハッシュが必要になる場合があります。 > 設定構造体(`Config`)は `connections_max`、`key_count`、`key_size_max`、`val_size_max`、`list_length_max` などのフィールドを公開し、派生アロケーションで接続ごとのバッファサイズを決定します。デフォルト設定(総計約748 MB、2048エントリ)では `val_size_max` または `list_length_max` を倍増すると、割り当て量が約2.8 GBに上昇する可能性があります。 > 今後の作業としては、カスタム静的コンテキストマップ実装の改善、より良いメモリ利用を実現する代替アロケータの探索、境界検査(fuzz)テストの追加による限界確認、および墓石再ハッシュ処理への対応が挙げられます。

2025/12/27 20:30

**フレームグラフ 対 ツリーマップ 対 サンバースト(2017)**

## Japanese Translation: **概要:** Flame グラフ(SVG)はディスク使用量を高レベルで明確に示します。たとえば、Linux 4.9‑rc5 では `drivers` ディレクトリが全容量の50%以上を占め、`drivers/net` サブディレクトリは約15%です。Tree マップ(macOS の GrandPerspective、Linux の Baobab)は非常に大きなファイルを素早く検出できますが、高レベルのラベルが欠けています;Baobab のツリー表示では各ディレクトリの横にミニバーグラフが表示されます。Sunburst(Baobab の極座標図)は視覚的に印象的ですが、角度で大きさを判断するため長さや面積よりも誤解しやすいです。他のツール―`ncdu` の ASCII バーと `du -hs * | sort -hr` ―はテキストベースで迅速なサマリーを提供しますが、同時に一階層のみ表示されます。 提案されたユーティリティは、これら三つの可視化(Flame グラフ(デフォルト)、Tree マップ、Sunburst)すべてを組み合わせるものです。Flame グラフは読みやすさ・印刷性・最小スペース使用量が優れているため、多数のサンプルファイルシステムでテストした後にデフォルトとして採用されます。このアプローチは、ディスク使用量を簡潔かつ印刷可能なスナップショットとして提供し、ユーザーや開発者がスペースを占有する項目をより効率的に検出できるよう支援します。アイデアは ACMQ の「The Flame Graph」記事と「A Tour through the Visualization Zoo」に引用された既存の研究に基づいています。 **反映された主なポイント:** flame グラフの高レベルビュー、Tree マップの大きなファイルを素早く検出できるがラベルが欠けている点、Sunburst の視覚的魅力とサイズ認識の問題、他ツールの制限、および提案ツールの三つのビュー(デフォルトは flame グラフ)と引用元への参照。

### なぜ Rust からアセンブリを呼び出すと、C からの呼び出しよりも遅くなることがあるか 手書きで作成したアセンブリルーチンを **C** から呼び出す場合、コンパイラは次のように最適化できます。 1. **プレーンで保護されていない呼び出しを生成** – 関数には Rust のランタイムオーバーヘッドがありません。 2. **呼び出し元のレジスタを保持** – 呼び出し規約(例:`__cdecl` や `__stdcall`)に従い、保護すべきレジスタと破壊されるレジスタを区別します。 3. **安全性チェックを行わない** – C はデフォルトで「unsafe」とみなすため、追加の検証が発生しません。 一方で同じアセンブリルーチンを **Rust** から呼び出すと、以下のような微妙な差異がオーバーヘッドを招くことがあります。 | 観点 | Rust 呼び出し | C 呼び出し | |------|----------------|------------| | **呼び出し規約** | デフォルトでは `extern "C"` がシステム ABI と一致しますが、`#[no_mangle]` を明示せずに使用すると、コンパイラが名前マングリングやシンボル解決を処理する小さなラッパーを追加することがあります。 | 直接 ABI を使用し、ラッパーは不要です。 | | **ABI の不一致** | Rust の関数シグネチャがアセンブリの期待するパラメータ(呼び出し規約・スタック整列・レジスタ使用)と合わない場合、コンパイラはスタック/整列を調整したり、引数を別順序で渡すコードを挿入します。 | C コンパイラは呼び出しのレイアウトを正確に知っています。 | | **安全性ラッパー** | Rust は `extern "C"` 境界を跨ぐ際に追加の安全チェック(例:NULL ポインタの検査、関数ポインタが NULL でないことの確認)を生成する場合があります。これらは微小ですがマイクロベンチマークでは影響します。 | C にはそのようなチェックはありません。 | | **インライン化と最適化** | アセンブリルーチン自体はインライン化できないため、Rust は通常の呼び出し命令を発行します。周囲の Rust コードが高度に最適化(例:ヘルパー関数の積極的なインライン化)されていると、呼び出しオーバーヘッドが目立ちます。 | C コンパイラは小さなラッパーをインライン化したり、一部チェックを削除してオーバーヘッドを減らすことがあります。 | | **リンカ / シンボル解決** | Rust のリンカーは、`#[link(name = "...")]` で正しく宣言されていない `extern` シンボルに対し追加のリロケーションエントリを生成する場合があり、一部プラットフォームでは実行時に微小な検索コストが発生します。 | C リンカーは通常、余分な間接参照なしでシンボルを直接解決します。 | #### 測定可能な遅延の典型的原因 1. **呼び出し規約の不一致** – 例:Rust が `extern "C"` を使用しているが、アセンブリは `__stdcall`(またはその逆)を期待している。 2. **スタック整列のズレ** – アセンブリルーチンが 16 バイト整列を前提としているのに、Rust の呼び出し側で 8 バイトのまま残っていると、コンパイラはスタック調整コードを挿入します。 3. **名前マングリング / シンボル検索** – `#[no_mangle]` を忘れると追加の名前解決ステップが発生し、別関数が呼び出される可能性があります。 4. **引数渡しの違い** – Rust がレジスタで引数を渡す一方、アセンブリはスタックを期待している(または逆)と、余分な移動命令が生成されます。 #### それらを比較可能にする方法 ```rust #[no_mangle] pub extern "C" fn my_asm_fn(arg1: i32, arg2: i64) -> i32 { // C の呼び出し規約に従ったインラインアセンブリ。 unsafe { asm!("...", in("rdi") arg1, in("rsi") arg2, out("eax") ret); } } ``` - **`extern "C"` を明示的に宣言** して、Rust が C コードと同じ ABI を使用するようにします。 - **`#[no_mangle]` を使う** ことで名前マングリングを防ぎます。 - **引数の順序・型を正確に合わせる** ことでアセンブリ側の期待と一致させます。 - **スタック整列を保証する**(必要なら `#[inline(always)]` を付けたり、手動で整列処理を入れたりします)。 これらが揃えば、Rust の呼び出しオーバーヘッドはほぼ C と同等になり、ベンチマーク上で差異が見えなくなるはずです。もしまだ違いが残る場合は、生成されたアセンブリをプロファイルして、余分な前処理/後処理コードが出力されていないか確認してください。 | そっか~ニュース