5月4日、Tweede Golf社のエンジニアが「Async Rust never left the MVP state」と題した記事を公開した。
この記事では、Async Rustが約束された「ゼロコスト抽象化」を実現できておらず、特に組み込みシステムでバイナリサイズが最大15倍に膨張するという衝撃的な問題が詳細に分析されている。IoT機器の普及により組み込み分野でのRust採用が急速に進む中、この問題は業界全体に大きな影響を与える可能性がある。
Async Rustが直面する深刻な現実
Rust言語が掲げる「ゼロコスト抽象化」は、高レベルな抽象化を使ってもパフォーマンス面でのオーバーヘッドがないという約束だった。しかし著者の分析によると、メモリが限られたマイクロコントローラーでは、この約束は全く守られていない。
著者が示す具体例は衝撃的だ:
fn foo() -> impl Future<Output = i32> {
async { 5 }
}
fn bar() -> impl Future<Output = i32> {
async {
foo().await + foo().await
}
}
この単純なbar関数は、非asyncバージョンなら23行のMIR(中間表現)で済むところを、360行のMIRを生成する。実に15倍以上の膨張だ。
無駄なパニック処理がメモリを圧迫
問題の核心は、Rustコンパイラが生成するステートマシンにある。bar関数は以下の状態を持つ:
variant_fields: {
Unresumed(0): [], // 開始状態
Returned (1): [], // 完了後の状態
Panicked (2): [], // パニック後の状態
Suspend0 (3): [_s1], // 1つ目のawait地点
Suspend1 (4): [_s0, _s2], // 2つ目のawait地点
}
特に問題なのはReturned状態の扱いだ。完了したFutureが再度pollされた場合、現在の実装ではパニックする。これは安全性のためだが、著者は「パニックではなくPendingを返せば十分」と指摘する。
著者は実際にRustコンパイラを改造してこの変更を試行し、組み込みファームウェアで2%-5%のバイナリサイズ削減を達成した。デバッグビルドではパニックを維持し、リリースビルドでは最適化するoverflow-checksのようなオプションの導入を提案している。
単純なコードでも生成される無駄なステートマシン
さらに深刻なのは、以下のような「即座に値を返すだけ」のコードでも不要なステートマシンが生成される点だ:
fn foo() -> impl Future<Output = i32> {
async { 5 }
}
手動でFuture traitを実装すれば:
impl Future for FooFut {
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(5) // 常に即座に値を返す
}
}
しかし現在のコンパイラは、この単純なケースでも状態の切り替えとパニック処理を含む複雑なMIRを生成してしまう。
LLVMの最適化にも限界
「LLVMが最適化してくれる」という期待も部分的にしか満たされない。著者はGodboltの実例で、fooが5を返すことをLLVMは理解するが、barの結果が10になることは最適化されないケースを示している。これは潜在的なパニック処理をコンパイラが完全に排除できないためだ。
Futureのインライン化問題
記事では、現在のRustコンパイラで生成されるFutureが決してインライン化されない重要な問題も指摘されている:
async fn foo(blah: bool) -> i32 { /* ... */ }
async fn bar(input: u32) -> i32 {
let blah = input > 10;
let result = foo(blah).await;
result * 2
}
現在はbarが独自のステートマシンを持ってfooを呼び出すが、**barはfooの状態をほぼそのまま使える**はずだ。適切なインライン化により、大幅な最適化が可能になる。
重複状態の統合で大幅削減も可能
記事は状態の重複問題も取り上げている:
pub async fn process_command() {
match get_command() {
CommandId::A => send_response(123).await,
CommandId::B => send_response(456).await,
}
}
このコードは同一の型を持つ2つの状態が生成され、456行のMIRになる。手動でリファクタリングすると302行に削減される。コンパイラが自動でこのような重複を検出・統合できれば大きな改善になる。
組み込み分野への影響と今後の展望
近年、Embassyなどのasync組み込みフレームワークの登場により、組み込み分野でのAsync Rust採用が加速している。メモリが数十KBしかないマイクロコントローラーでは、これらのオーバーヘッドはプロジェクトの成功を左右する致命的な問題になりかねない。
著者はこれらの問題を解決するため、Rustの2026年プロジェクトゴールを提出している。この提案はコンパイラレベルでのasync最適化を目指すものだ。
Async Rustは確かに素晴らしい技術だが、記事が示すように「MVPレベルから抜け出せていない」という厳しい現実がある。特に成長著しい組み込み分野では、これらの最適化は単なる改善ではなく、Rust言語の将来を左右する重要な課題と言えるだろう。
詳細はAsync Rust never left the MVP stateを参照していただきたい。