1 ポイント 投稿者 GN⁺ 2026-05-06 | 2件のコメント | WhatsAppで共有
  • Async Rust は、エグゼキュータに依存しないコードをサーバーとマイクロコントローラの両方で動かせるが、コンパイラが生成する状態機械のため、とくに組み込みではバイナリサイズの増加が目立つ
  • bar() のように await 箇所が2つある単純な例でも、360行のMIRUnresumed, Returned, Panicked, Suspend0, Suspend1 の状態が生成され、同期版では23行しか必要ない
  • 完了した future を再度 poll したとき、panic の代わりに Poll::Pending を返すよう変更すれば、unsafe な動作なしで契約を満たせ、実験では組み込みファームウェアの バイナリサイズが2%〜5%減少 した
  • await のない async { 5 } でも現在は基本3状態の状態機械が作られるが、毎回 Poll::Ready(5) を返すよう最適化すると、組み込みバイナリサイズが 0.2%減少 した
  • 提案された Project Goal は、リリースモードでの完了後 panic の除去、await のない async block の状態機械除去、単一 await future のインライン化、同一状態の畳み込みをコンパイラで進める取り組みである

Async Rustのコンパイラレベルの肥大化問題

  • Async Rust は、エグゼキュータに依存しないコードをサーバーとマイクロコントローラで同時に実行できるようにするが、小さなマイクロコントローラではバイナリサイズの増加がとくに目立つ
  • Rustブログは async/await を ゼロコスト抽象化 として紹介したが、async は実際には大きな肥大化を生み、同じ問題はデスクトップやサーバーにもあるものの、メモリや計算資源が多いため表面化しにくい
  • async コード記述時の肥大化を避ける 回避策 に続き、この問題をコンパイラ側で解決するための Project Goal が提出された
  • future が必要以上に大きくなり、コピーが増える問題は対象外となっている

生成される future の構造

  • 例のコードでは、foo()async { 5 } を返し、bar()foo().await + foo().await を実行する
  • bar には await 箇所が2つあるため、状態機械には少なくとも2つの状態が必要だが、実際にはさらに多くの状態が生成される
  • Rust コンパイラは複数のパスで MIR をダンプでき、coroutine_resume パスは最後の async 専用 MIR パスである
    • async は MIR には残るが LLVM IR には残らないため、async が状態機械へ変換される過程は MIR パスで起きる
  • bar 関数は 360行のMIR を生成し、同期版は 23行 しか使わない
  • コンパイラが出力する CoroutineLayout は、実質的に enum 形式の状態集合である
    • Unresumed: 開始状態
    • Returned: 完了状態
    • Panicked: panic 後の状態
    • Suspend0: 1つ目の await 箇所で、foo future を保持する
    • Suspend1: 2つ目の await 箇所で、1つ目の結果と2つ目の foo future を保持する
    広告
  • Future::poll は安全な関数なので、future がすでに完了した後に再び呼ばれても UB を起こしてはならない
    • 現在は Suspend1 の後に Ready を返し、future を Returned 状態に変える
    • この状態で再度 poll すると panic が発生する
  • Panicked 状態は、async 関数が panic した後に catch_unwind でそれを捕捉した際、その future を再度 poll できないようにするための状態に見える
    • panic 後の future は不完全な状態である可能性があるため、再度 poll すると UB につながりうる
    • この仕組みは mutex poisoning と非常によく似ている
    • Panicked 状態に関するこの解釈は、確かな文書を見つけにくく、90%ほどの確信にとどまる

完了後の poll で本当に panic が必要か

  • Returned 状態の future は現在 panic するが、必ずそうである必要はない
    • 必要条件は UB を起こさないことだけである
  • panic は比較的高コストであり、最適化で除去しにくい副作用のある経路を追加する
  • 完了済み future を再度 poll したときに Poll::Pending を返せば、unsafe な動作なしで Future 型の契約を満たせる
  • コンパイラを修正してこの方式を試したところ、async な組み込みファームウェアで 2%〜5%のバイナリサイズ削減 が確認された
  • この動作は、整数オーバーフローにおける overflow-checks = false のように、スイッチとして提供する案が提案されている
    • デバッグビルドでは誤った動作を即座に露呈させるため、引き続き panic する
    • リリースビルドでは、より小さな future を得られる
  • panic=abort を使う場合は、Panicked 状態自体を削除できる可能性があり、その影響には追加検討が必要である
広告

await がなくても常に状態機械が生成される

  • foo()async { 5 } を返すだけなので、手動実装での最適な形は、状態を持たず常に Poll::Ready(5) を返す future である
  • しかし、コンパイラが生成した MIR には Unresumed, Returned, Panicked という基本3状態が依然として存在する
    • poll 時に現在状態の discriminant を確認して分岐する
    • 完了後に再度 poll すると `async fn` resumed after completion アサートで panic する
  • この場合、状態機械を作らず毎回 Poll::Ready(5) を返すよう最適化できる
  • これをコンパイラに試験的に適用したところ、組み込みバイナリサイズが 0.2%減少 した
    • 削減幅は大きくないが、単純な最適化なので適用する価値がある可能性がある
  • この最適化は挙動を少し変えるが、影響を受けるのは規約を守らないエグゼキュータだけである
    • 現在のコンパイラでは、その後の poll で panic する
    • 最適化後は future が常に Ready を返す

LLVM だけでは不十分

  • MIR の出力が非効率でも LLVM がすべて整理してくれる場合はあるが、条件は限られる
    • future が十分に単純であること
    • opt-level=3 を使うこと
  • future が複雑になると LLVM は除去できず、慣用的な async Rust コードでは future が深くネストするため、複雑さはすぐに大きくなる
  • 組み込みや wasm のようにサイズ最適化をよく行う環境では、LLVM がこれをすべて最適化できない
  • Godbolt の例: https://godbolt.org/z/58ahb3nne
    • 生成されたアセンブリでは、LLVM は foo が 5 を返すことは理解しているが、bar の答えを 10 に最適化できていない
    • foo の poll 関数呼び出しも残っている
    • これは、コンパイラが完全には把握できない潜在的な panic 経路があるためである
    • LLVM は foo が実際には1回しか呼ばれず、panic しないことを知らない
  • IR で panic 分岐をコメントアウトすると、よりよく最適化される: https://godbolt.org/z/38KqjsY8E
  • LLVM に事後最適化を期待するより、コンパイラが LLVM により良い入力を与えるべきである
広告

future のインライン化がうまくいかない

  • インライン化はその後の最適化パスを可能にするため重要だが、生成される Rust future は現在、早い段階ではインライン化されない
  • 各 future が実装を得た後で LLVM とリンカがインライン化の機会を得るが、前述の問題のため、その時点では遅すぎる
  • 最も直接的なインライン化の機会は、bar() が単に foo(blah).await だけを行う形である
    • trait を使って抽象化を作るときによく現れるパターンである
    • 現在のコンパイラは bar 用の状態機械を作り、その内部で foo 状態機械を呼び出す
    • より効率的には、barfoo future 自体になれる
  • preamble と postamble がある場合はより複雑になる
    • 例: bar(input)input > 10 から blah を作り、foo(blah).await した後、その結果に * 2 を適用する
    • async 関数を別シグネチャへ変換するとき、とくに trait 実装でよくある
  • この形の bar でも、それ自体の async 状態は不要である
    • 単一の await 箇所を越えて保持されるデータが、foo にキャプチャされた値以外にないためである
    • ただし bar が単純に foo 自体になることはできず、状態の大部分を foo に依存できる
  • 手動実装では、BarFutUnresumed { input }Inlined { foo: FooFut } の状態を持てる
    • 最初の poll で preamble を実行して foo(blah) を作り、Inlined 状態に切り替える
    • 以後は foo.poll(cx) の結果に postamble を適用する
  • 最初の await 箇所までコードを先に実行できれば Unresumed 状態も除去できるが、future は poll される前には何もしないことが 保証 されているため、変更できない
  • poll 中の future の特性を問い合わせられれば、追加のインライン化最適化が可能になる
    • たとえば、その future が最初の poll で常に ready を返すと分かれば、呼び出し側 future ではその await 箇所の状態を作る必要がない
    • こうした最適化を再帰的に適用すれば、多くの future をはるかに単純な状態機械へ畳み込める
  • 現在の rustc の構造では、各 async block が個別に変換され、その後に関連データが保持されないため、このような問い合わせはできないように見える
  • future のインライン化はまだ実験されていないが、バイナリサイズと性能に大きく寄与すると期待される
広告

同一状態の畳み込み

  • async block の各 await 箇所ごとに、状態機械には追加の状態が生じる
  • 次のようなコードは自然だが、2つの分岐で同じ async 関数を await するため、同一の状態が2つ生じる
    • CommandId::A => send_response(123).await
    • CommandId::B => send_response(456).await
  • この場合、CoroutineLayout には send_response の同じ coroutine 型を保持する _s0, _s1 がそれぞれ生じ、Suspend0, Suspend1 の2状態が作られる
  • この関数の MIR は 456行 で、多くの基本ブロックが実質的に重複している
  • まず応答値だけを計算し、その後で一度だけ send_response(response).await するよう手動でリファクタリングすると、重複状態はなくなる
    • CommandId::A123
    • CommandId::B456
    • その後で send_response(response).await
  • リファクタリング後の CoroutineLayout には保持される future が1つだけあり、Suspend0 状態だけが残る
  • MIR 全体の長さは 302行 に減り、重複が消える
  • したがって、同一のコード経路と状態を見つけて1つに畳み込む最適化パスは有用に見える
    • この最適化は future インライン化パスとうまく組み合わせられる可能性がある

実験リンクと追加ベンチマーク

Project Goalへの支援要請

  • この作業はコンパイラ側で進めるため、Project Goal として提出されている: https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
  • 資金がなければ多くの作業を進めるのは難しいため、この取り組みの恩恵を受ける企業や組織による部分的または全面的な支援が必要である
  • 連絡先は dion@tweedegolf.com
  • 作業範囲と必要な資金規模は柔軟だが、€30k あれば全体またはかなりの部分を完了できると見積もられている

2件のコメント

 
GN⁺ 2026-05-06
Hacker Newsのコメント
  • タイトルはやや大げさだという点には同意するが、本文はよく書けていて要点もよく伝わっている。
    Rust async について強い意見を言えるほどの経験はまだないが、いくつか目についた点がある。
    良い点は、明示的なランタイムを置けることだ。プロジェクト全体を async で汚染するのではなく、基本は同期のままにして、入出力の「境界」でだけランタイムを使える。
    いま取り組んでいるプロジェクトにはこのやり方がよく合っていて、Zig が入出力コードで取っている戦略にもかなり似ているように見える。この場合、関数カラー問題もほとんど解決されたし、入出力と CPU 中心のコードを厳密に分離する必要があったので、明示的な入出力ランタイムは自然だった。
    悪い点は、エコシステム全体が tokio に依存しすぎているように見えることだ。Java で GC が選択制なのに、実際にはみんな同じサードパーティの GC ランタイムを使っていて、どんなライブラリを持ってきてもそのランタイムを強制される、というのに近い。こうした中央集権的な依存は健全ではない。

    • 文脈によってはエコシステム全体が tokio に依存しているように見えるかもしれないが、組み込み Rustを見るともう少し納得できる。
      ワークステーション向けプロセッサでの async ランタイム要件と、RP2040 のような環境での要件は大きく異なる。それでもバックエンドを差し替えられるので、小さな ARM M0 マイクロコントローラ向けに async 入出力コードを書くときでも、組み込み向けランタイムの embassy を使えば、ほかの環境で使うコードとほとんど同じ見た目になる。
      同じ trait とインターフェースを使うので、ランタイムの詳細をあまり意識しなくて済む。小さな RTOS を使ったり、自前で async 環境を作ったりするのに比べるとかなり良い。
      embassy で async コードを書いて学んだことは、ほかの領域にも持っていける。
    • 代替が何なのか気になる。自分は tokio を使って満足しているが、ほかの人たちが smolasync-stdglommio のような別の executor を使うのも良いことだ。
      tokio は標準ライブラリの一部ではないにせよ、きちんと保守されているので、今の状況は悪くないように見える。むしろ標準ライブラリに入ってしまうと、別の executor を使いにくくなり、標準ライブラリを他プラットフォームへ移植するのも難しくなるのではないかと心配している。
      もちろん、この心配は杞憂かもしれない。
    • Java の話が出たのは興味深い。Java も歴史的に似た問題を抱えてきた。
      ロギングは今では slf4j にだいぶ収束したが、いまだに別のものを使うライブラリもあるし、共通ユーティリティは最初は Apache Commons で、今は Guava が多い。
      JSON は Jackson にある程度まとまったが、Gson や Simple-json も一般的だし、null 許容性アノテーションも、正式化されなかった JSR-305 の非公式配布版から checker framework を経て、最近では JSpecify へ移行しつつある。
      こうした基本要素は言語側が提供しないと、断片化や事実上の標準ライブラリ乱立を避けられない。
    • async を使いつつも tokio に依存せず Rust を活かせる領域は多い。実際、完全に tokio に縛られているのは Web/サーバー周りに近いように見える。
      ライブラリを executor 非依存で書くのはそれほど難しくないが、継続的な注意が必要で、コミュニティ全体で常に守られているわけではない。
  • 素晴らしい記事だ。こういう最適化の深掘りは好きだし、プロジェクト目標もうまく進んでほしい。
    コンパイラは「些細な」ケースの最適化に、しばしばあまり大きな労力を割かないように感じたことがある。
    ただ、タイトルは内容に比べて劇的すぎる。「Async Rust Optimizations the Compiler Still Misses」でも十分クリックしたと思う。

    • タイトルは単に事実だからそうした。async は 2019 年ごろに入って以来、大きく変わったことがあまりない。
      いまでは trait やクロージャで async を使えるようになったが、それは型システムの更新であって、async の機械そのものの変化ではない。Waker も少し扱いやすくなったが、どちらかといえば std/core 側の改善に近い。
      私の理解では、async Rust を着地させた人たちはかなり燃え尽きて活動が減り、その後を引き継いだ人はほとんどいない。ただ、Google の人たちがキャプチャされた変数のメモリ配置を最適化する PR を 1 本出しているのはかなり嬉しい。
      自分と同僚は async を多用しているので、もしかすると自分たちでやるか、少なくとも始めるべきなのかもしれない。「無料」は子犬を飼う意味での無料に近い。
      だからタイトルが少し釣り気味なのはその通りだが、それでもそのタイトルを撤回するつもりはない。
    • タイトルが大げさすぎるという点には同意する。
      著者は些細な関数オーバーヘッドにこだわりすぎているように見える。「panic」と「返却済み」状態のオーバーヘッドを気にしているが、それは大きな問題ではない。
      有用な async ブロックの大半は十分に大きいので、エラーケースのオーバーヘッドは埋もれる。
      インライン化不足については一理あるかもしれない。ただ、大量のアクティビティ数を制限するのはたいてい各アクティビティが必要とする 状態空間だ。
  • async は全体として、まだ未熟なアイデアに見える。普通のコードももともと非同期だった。
    async な作業を待つ必要があれば、スレッドは準備できるまで眠り、カーネルがそれを抽象化してくれる。ところが論理スレッドとしてコードを構成するのを嫌って、イベント用のコールバックシステムを追加し、その後コールバックは推論しづらく、逐次制御のほうが良いと気づいた。
    だから スレッドこそ正しいプログラミングモデルだったのだと思う。
    いまや言語ランタイムは移植性と性能のために「グリーンスレッド」を好むが、多くの言語はそれをきちんと提供していない。代わりに async/non-async のカラー問題、スケジューリング、優先度、非プリエンプティブといった問題が出てくる。1970 年代より悪いスケジューリングとプロセスモデルだ。

    • 「普通のコードもすでに async で、待つときはスレッドが眠りカーネルが抽象化する」というのは正確ではない。
      async コードであっても、表現可能な並行性を最大化しない書き方がよくある。たとえば「N 個の入出力作業を全部同時に実行する」ではなく、「各作業 X について await process(x) する」ような形だ。
      しかしスレッドの世界では、この並行性の問題はさらに深刻になる。スレッドは本質的に重すぎて、並行性を効率よく表現しにくく、その方向に最適化する術もない。
      これは新しい教訓ではない。ワークスティーリング executor が従来のスレッドよりはるかに低レイテンシで、P99 も安定することは昔から知られていた。2000 年代初頭に Apple が GCD を作った理由もそこにある。
      スレッドは、カーネルスケジューラが負荷を理解するのに必要な豊富な情報を渡せず、カーネルスレッドは細粒度の並行性を得るには重すぎる仕組みだ。純計算ではなく、入出力や混合負荷ではなおさら悪い。
      すべてのプログラムにこの水準の性能が必要なわけではないが、同じ労力でより高い性能基準を達成するのははるかに容易で、実際、従来のやり方では追いつきにくいレイテンシやスループットが得られる。
      async の方向性が正しいことは io_uring にも表れている。カーネルの高性能 I/O 手法である io_uring は、従来のスレッディングやシステムコールとはまったく異なり、完了処理も async 並行性にずっと近い。ただし async/await だけでは async タスク間の関係を表現するにはカラーの数が足りず、完全に活用するのはより難しい。
    • カーネルと OS スケジューラが介在した瞬間、本来出せるはずの速度より 3〜4桁 遅くなることがある。
      最後にコルーチン/スケジューリングのコードを触ったときは、すぐ終了するスレッドを作って join するのに約 200µs かかり、自前のグリーンスレッドを作ってスケジューリングして待つのは約 400ns だった。
      誰かがまた absurd に複雑な async フレームワークを設計するのを 10 年待つ必要はない。どんなシステム言語でも、アセンブリ 20 行あれば自分で グリーンスレッド/スタック付きコルーチンを作れる。
    • 「スレッドが正しいプログラミングモデル」かどうかは、何をするかによる。計算中心の処理にはスレッドが向いていて、帯域中心の処理には async が向いている。
      帯域中心コードの最適化はスケジュール設計の問題だ。古典的なマルチスレッドモデルではスケジューリングを制限付きでしか制御できないが、async モデルではほぼ完全に制御できる。
      よく最適化された async スケジュールは、同じ帯域中心タスクにおいて同等のマルチスレッドアーキテクチャよりずっと速く、比べものにならない。
      今日の高性能コードの多くは帯域中心であり、async はこうしたワークロードをより最適化しやすくするために存在している。
    • コールバックのほうがむしろ推論しやすいと思う。
      並行処理をテストして競合状態を適切に処理できるか確認するとき、コールバックはスケジューリングを制御できるのでずっと簡単だ。各コールバックが分離された単位を表すので、どのイベントを並べ替えられるかが見え、さまざまな順序を検討しやすい。
      一方スレッドでは順序を無視しやすく、他スレッドで生じる複雑さが、いまのスレッドにいつ影響しうるのかを考えなくなりがちだ。単純なのではなく、単純化されているに近い。
      また、人為的なバリアを入れてスレッドを止めたり、I/O をスタブ化して順序を制御するコールバック付きの mock を渡したりしない限り、並行シナリオを実際に変えてテストするのは難しい。
      コールバックの問題は、キャプチャされたコールスタックが論理的なコールスタックではないことだ。コールスタックを意味あるものにする努力をした一部のライブラリ/ランタイムでない限り、良いエラー定義が必要になる。
      もちろん、2 つのパラダイムを混ぜて両方の欠点だけを得ることもある。
    • スレッドは async+コールバックより優れているとか劣っているとかではなく、別のモデルだ。スレッドに向く問題もあれば、async で表現したほうがはるかに良い問題もある。
  • Rust の主目的が安全性なら、なぜ panic があるのか理解できない。コードに panic しうる経路が一切ないことを証明できるべきだ。
    今週ずっと調べていたが、決して panic しないことを保証するプログラムを作るのは非常に難しい。自分の理解では panic ハンドラは約 300KB あり、これを除外する唯一の方法は、コンパイル時点でコード内に panic しうる経路がまったく存在しないことだ。コンパイル後のバイナリに panic ハンドラが入っているか確認するやり方はハックっぽく感じる。
    unwrap や他の panic 操作を lint で禁止することはできるが、no-panic Rust の部分集合があれば、この記事で扱われた問題のかなりの部分は消えていただろう。
    実際にはビット反転でも起きない限り発生しないような状況なのに、理論上 panic しうる演算が多すぎる言語を扱うのはもどかしい。配列が空でないことの証明や async の扱いでも同じだ。
    結局、決して起きないはずの状況のために大量のエラー処理を書くか、先頭要素と残りのリストを分ける空でないリスト・パターンのような奇妙な構造を使うことになる。そしてその構造自体も独自の肥大化を持ち込む。

    • Rust-in-Linux では、失敗しうるメモリ操作のようなものを通じてこの問題に取り組んでいる。彼らにとっては必要な機能だ。
      配列が空でないことの証明のようなものも含め、証明ベースの利用を増やす作業もゆっくり進んでいる。
    • panic は使い勝手と安全性の両面でかなり重要だ。
      panic がなく、どんな状況でも実行を継続しなければならないなら、不変条件が壊れたメモリ破壊のような状況から復旧しようとして、不変条件を検査するあらゆる場所に大量のエラー処理を書かなければならない。
      それは、あなたが懸念しているまさにその問題、つまりほとんど絶対に起きない状況のための膨大なエラー処理とまったく同種のものだ。
    • Rust の目標は メモリ安全性だ。panic はメモリ安全性の観点では完全に安全だ。
    • プログラムを動かす OS 自体ですら完璧ではない。
      ツールがすべてを失敗不能にしてくれるのを望み、自分では何もしたくないという態度にはかなりうんざりする。簡単な API を望み、それで十分簡単でなければ YAML で「プログラミング」する Kubernetes コンテナを望み、それでも足りなければ GCP や Amazon のクリック型ホスティングサービスを望むようになる。
      結局それは、プログラミングしたいというより、失敗しないアプリを消費したいだけに近く、そういうライフスタイルは何かを作ってくれる人との共生関係の上にあるだけだ。
  • こういう醜いが必要な議論は C++ でもしばらく続いてきた。
    Rust に async が導入された当初から、その伝染的な性質は好きではなかった。
    Rust にはうまくいってほしいし、こういう人たちが増えれば Rust の将来も明るくなるかもしれない。

  • 最近 Rust async の作業を始めたが、いま直面している主な問題は コード重複だ。
    非同期 API とブロッキング API の両方をサポートしたい関数は、どれも重複して書かなければならない。maybe-async のようなものがあると良さそうだ。
    回避策として maybe-async や bisync のような crate を見てみたが、どれも問題や強い制約があった。

    • asyncconst のようなキーワードに対して関数をジェネリックにできる キーワードジェネリクス の作業が進んでいる。
      いま同期/非同期の両方にまたがるコードを書く最善の選択肢は sans-io だ。Fireguard の Thomas Eizinger がこのパターンについて良い記事を書いている[1]。
      このパターンは sync/async 問題をきれいに解決するだけでなく、テストも容易にし、DST のような手法への道も開く[2]。
      この話題について私が書いた記事もあるが[3]、問題は async 対 sync を超えて、異なる executor 同士まで含むもっと広いものだと強調している。
      0: https://github.com/rust-lang/effects-initiative
      1: https://www.firezone.dev/blog/sans-io
      2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
      3: https://hugotunius.se/2024/03/08/on-async-rust.html
    • 実際に何をしているかに大きく依るが、十分に単純なら、型と await を差し替える マクロを作れるかもしれない。
    • 古典的な 関数カラー問題だ。https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
    • 私の見方では、async 関数はすでに maybe-async だ。
      fn -> voidfn -> Future の違いは、前者はすぐ最後まで実行され、後者は後になって終わるかもしれないという点だ。
      async 関数をブロッキング方式で実行したいなら、ブロッキング executor を使えばいい。
  • この記事が気に入った理由は、2026 年 Rust 目標まで見られたことだ。
    チームで Rust を使っているが、必要なことをするためにそこまで深く潜る必要はなかった。それでも、コミュニティからのフィードバックが多い言語が土台から発展していく様子を見るのは楽しい。
    C++ ではこういう流れをあまり感じなかったし、ほかの分野でどう回っているのかもよく知らない。
    ただ残念なのは、目標ごとに特定の資金調達が必要に見えて、少し Kickstarter 的に感じることだ。今のところ見つかっている最善のモデルがこれなのだろうか。

    • 「プロジェクト目標」という用語は、実際の意味に比べてかなり誤解を招く。
      プロジェクト目標は、ある個人や小さなグループが特定の作業をしたいと表明し、Rust プロジェクトのボランティアにコードレビューや質問への回答など、継続的な支援時間を求める仕組みだ。
      それは Rust プロジェクト自体がその目標を立てたとか、必ず支持したという意味ではない。
      したがって、これを Rust の公式ロードマップと見るのは適切ではなく、「この領域で作業したい貢献者がいる」程度に見るのがより正確だ。
    • C++ ISO 委員会の内部でも、その言語の進化プロセスがある程度壊れているという合意があるようだ。主に規模と組織のされ方の問題だ。
      技術が商業的に定着すると、残念ながらこういう流れになることが多いように思う。大口スポンサーが自分たちの関心ある部分だけを支援するのを責めるのも難しい。
      幸い、TweedeGolf のかなりの資金はオランダ政府から来ていると理解している。
    • オープンソースの仕事には大きく 2 種類あるように思う。機能開発と保守だ。
      新機能は「売る」ことができる。作るのに費用はかかるが、現実の問題を解決し、その問題のコストが機能開発のコストより大きければ、企業はたいてい支払う意思がある。
      保守はもっと難しいが、いまはメンテナーファンドもある。RustNL のファンドがその例だ: https://rustnl.org/maintainers/
      こうしたファンドは、より広く継続的な作業を対象とし、複数の組織が少しずつ拠出して支える。
      これが最善のモデルかは分からないが、少なくともある程度は機能しているようだ。
  • Rust Async と Tokio のドキュメントを読めば、CPU 集約的な部分を async スタックに入れてはいけない理由、std::sync::Mutex のような基本ツールを async ブロック内で効率よく使う方法、同期コードと async コードをつなぐ方法がきちんと説明されている。
    多くのコードは効率に関心がないか、関心があっても必要ないので、こうした指針に従わない。しかし、性能と効率を重視するプロジェクトは多く、コードが本番で動けば落とし穴に気づく。ScyllaDB がその一例だ。
    LLM も助けにならない。何もかも main まで async にしてしまい、基本ツールの選択を誤り、システムをきちんと設計しない。

  • 重複した状態の折りたたみ、つまり process_command の例のように match を await 分岐の外へ引き上げるパターンは、今日ある既存の async コードに誰でも適用できる最も簡単な方法だ。
    コンパイラ作業は不要で、リファクタリングするだけでよい。

    • 少なくとも、どこに適用できるか見つけてくれる カスタム lint は必要だろう。その程度なら、かなりコンパイラ作業に近い。
  • 「Future は簡単にはインライン化されない」という点について、自分が作ったプログラミング言語では、async 関数内の async 関数呼び出しをインライン化するカスタムパスを書いた。
    だいたいうまく動いて、一部のボイラープレートも減らせるが、生成されるバイナリサイズはかなり大きくなる。
    技術的には Rust でも同じことはできる。

 
GN⁺ 2026-05-06
Lobste.rs の意見
  • タイトルだけ見て予想していたよりもずっと 建設的な記事 だった

    • 単に事実に近いと思う。MVP リリースから 7 年が経ったが、言語設計やコンパイラ実装ではほとんど進展がなく、MVP を主に作り上げた人たちが同じ頃にプロジェクト活動を減らしたことで、その後の引き継ぎが止まってしまった状態だと思う
      この作業をやろうとしている人が必要な支援を受けられるといいと思う
  • I want to work on this in the compiler and as such have submitted it as a Project Goal

    Stop generating statemachines that don’t have to be there
    Make the compiler’s job easier by removing panic paths and branches
    Make statemachines smaller

    この問題が扱われているのは良いことだと思う。今の rustc が LLVM にあまりにも多くのコード を渡して、最適化器が全部何とかしてくれることを期待している、という話は何度か見たことがあるが、特にこの記事ではその作業のための 資金支援 まで求めている

  • しまった、自分は勘違いしていた
    async はどんな形であれランタイム、タスク追跡、完了を確認する ポーリング が必要なので、本質的に「重い」ものだとずっと思っていた。オーバーヘッドは 0 ではないのだから
    ここで言う「ゼロコスト抽象化」は言語機能の話であって、付随するランタイムとは別だと考えていた
    LLVM に渡す前に rustc が何を出力しているのか を見てみようとすら思わなかった

  • async Rust に慣れていない人向けに言うと:

    It's amazing how we can write executor agnostic code that can run concurrently on huge servers and tiny microcontrollers.

    これは本当にその通り。async 呼び出しが入れ子になったツリーも、最大限に最適化されると、内部に状態機械を持つ 単一の構造体 へと固まる。実に巧妙なやり方だ

  • リリースビルドでこのケースに到達すると、ある種の デッドロック になるのか? それとも常に Pending のタスクを待つタスクのせいでリークが起きる可能性もあるのか?

    • その通り。そうした future は 停止した状態 になって決して完了しない。ただし、その状態に到達できるのは、すでにバグのある低レベル async コードだけで、完了した future を正しく追跡できないコードは、おそらくすでにリークやデッドロックを起こしている可能性が高い
      .await では誤ったポーリングはできない
  • いくつか思うところがある:

    1. この記事は、より多くの最適化ロジックを LLVM の外に出して MIR 層 に移すべきだという主張のように見える。たとえば async 関数のインライン化が LLVM より MIR で簡単な理由は理解できる。async について MIR で実現できたなら、そのロジックを同期関数にも一般化して、LLVM の最適化パスの一部を取り除くこともできるのではないかと思う。大仕事なのは分かっているし、実務的な質問というより方向性の話に近い。フロントエンド/ミドルエンドのコンパイラがある程度複雑になれば、LLVM の汎用最適化のかなりの部分は別の場所へ移したほうがよくなるのかもしれない
    2. 依然として panic=unwind は好きになれない。一部のテストハーネスを除けば、panic=abort より優れていてコストを相殺できるほどの利点をほとんど見たことがない。テストハーネスですら、Linux ではやや難解な clone を使って pthread_join の代わりに実行スレッドを wait するような形で、似た選択を適用できそうに思える。この点は自分が間違っているかもしれない
  • リンク、他の人のところでもさっき死んだ?
    修正: ブログ記事が 0.5 秒くらい表示されたあと 404 ページ に飛ばされる
    修正 2: ブログ記事一覧に入っていろいろクリックしてみたが、一覧にあるその記事を開いても 404 ページに行く。静的ページか、少なくともそうあるべきブログをどうやったらこんなふうに壊せるんだ?

    • 少し不必要に無礼で攻撃的な言い方に感じる。ウェブサイトにもバグはあり得るし、報告すること自体は有用だけれど、このコメントはちょっと意地が悪く聞こえる
      参考までに、同じ再現手順を試してみたようだが、自分の環境では 404 はまったく出なかった。スマホとデスクトップで、JavaScript をオン/オフの両方で試した。だから、起きていた現象は見た目より複雑だったのかもしれない