Async RustはMVP状態を一度も脱していない
(tweedegolf.nl)- Async Rust は、エグゼキュータに依存しないコードをサーバーとマイクロコントローラの両方で動かせるが、コンパイラが生成する状態機械のため、とくに組み込みではバイナリサイズの増加が目立つ
bar()のように await 箇所が2つある単純な例でも、360行のMIR とUnresumed,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 が必要以上に大きくなり、コピーが増える問題は対象外となっている
- この問題はすでに知られており、一部を扱う PR が公開されている: https://github.com/rust-lang/rust/pull/135527
生成される future の構造
- 例のコードでは、
foo()がasync { 5 }を返し、bar()がfoo().await + foo().awaitを実行する- Godbolt の例: godbolt
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 箇所で、foofuture を保持するSuspend1: 2つ目の await 箇所で、1つ目の結果と2つ目のfoofuture を保持する
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 しないことを知らない
- 生成されたアセンブリでは、LLVM は
- IR で panic 分岐をコメントアウトすると、よりよく最適化される: https://godbolt.org/z/38KqjsY8E
- LLVM に事後最適化を期待するより、コンパイラが LLVM により良い入力を与えるべきである
future のインライン化がうまくいかない
- インライン化はその後の最適化パスを可能にするため重要だが、生成される Rust future は現在、早い段階ではインライン化されない
- 各 future が実装を得た後で LLVM とリンカがインライン化の機会を得るが、前述の問題のため、その時点では遅すぎる
- 最も直接的なインライン化の機会は、
bar()が単にfoo(blah).awaitだけを行う形である- trait を使って抽象化を作るときによく現れるパターンである
- 現在のコンパイラは
bar用の状態機械を作り、その内部でfoo状態機械を呼び出す - より効率的には、
barがfoofuture 自体になれる
- preamble と postamble がある場合はより複雑になる
- 例:
bar(input)がinput > 10からblahを作り、foo(blah).awaitした後、その結果に* 2を適用する - async 関数を別シグネチャへ変換するとき、とくに trait 実装でよくある
- 例:
- この形の
barでも、それ自体の async 状態は不要である- 単一の await 箇所を越えて保持されるデータが、
fooにキャプチャされた値以外にないためである - ただし
barが単純にfoo自体になることはできず、状態の大部分をfooに依存できる
- 単一の await 箇所を越えて保持されるデータが、
- 手動実装では、
BarFutはUnresumed { input }とInlined { foo: FooFut }の状態を持てる- 最初の poll で preamble を実行して
foo(blah)を作り、Inlined状態に切り替える - 以後は
foo.poll(cx)の結果に postamble を適用する
- 最初の poll で preamble を実行して
- 最初の 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).awaitCommandId::B => send_response(456).await
- この場合、
CoroutineLayoutにはsend_responseの同じ coroutine 型を保持する_s0,_s1がそれぞれ生じ、Suspend0,Suspend1の2状態が作られる - この関数の MIR は 456行 で、多くの基本ブロックが実質的に重複している
- まず応答値だけを計算し、その後で一度だけ
send_response(response).awaitするよう手動でリファクタリングすると、重複状態はなくなるCommandId::Aは123CommandId::Bは456- その後で
send_response(response).await
- リファクタリング後の
CoroutineLayoutには保持される future が1つだけあり、Suspend0状態だけが残る - MIR 全体の長さは 302行 に減り、重複が消える
- したがって、同一のコード経路と状態を見つけて1つに畳み込む最適化パスは有用に見える
- この最適化は future インライン化パスとうまく組み合わせられる可能性がある
実験リンクと追加ベンチマーク
- 2つの実験を同時に適用すると、
smolエグゼキュータを使った x86 の合成ベンチマークで約 3%の性能向上 が出る - No panics in poll after ready: https://github.com/rust-lang/rust/compare/main...diondokter:rust:resume-pending
- No await, no statemachine: https://github.com/rust-lang/rust/compare/main...diondokter:rust:no-statemachine-when-no-await
Project Goalへの支援要請
- この作業はコンパイラ側で進めるため、Project Goal として提出されている: https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
- 資金がなければ多くの作業を進めるのは難しいため、この取り組みの恩恵を受ける企業や組織による部分的または全面的な支援が必要である
- 連絡先は
dion@tweedegolf.com - 作業範囲と必要な資金規模は柔軟だが、€30k あれば全体またはかなりの部分を完了できると見積もられている
2件のコメント
Hacker Newsのコメント
タイトルはやや大げさだという点には同意するが、本文はよく書けていて要点もよく伝わっている。
Rust async について強い意見を言えるほどの経験はまだないが、いくつか目についた点がある。
良い点は、明示的なランタイムを置けることだ。プロジェクト全体を async で汚染するのではなく、基本は同期のままにして、入出力の「境界」でだけランタイムを使える。
いま取り組んでいるプロジェクトにはこのやり方がよく合っていて、Zig が入出力コードで取っている戦略にもかなり似ているように見える。この場合、関数カラー問題もほとんど解決されたし、入出力と CPU 中心のコードを厳密に分離する必要があったので、明示的な入出力ランタイムは自然だった。
悪い点は、エコシステム全体が tokio に依存しすぎているように見えることだ。Java で GC が選択制なのに、実際にはみんな同じサードパーティの GC ランタイムを使っていて、どんなライブラリを持ってきてもそのランタイムを強制される、というのに近い。こうした中央集権的な依存は健全ではない。
ワークステーション向けプロセッサでの async ランタイム要件と、RP2040 のような環境での要件は大きく異なる。それでもバックエンドを差し替えられるので、小さな ARM M0 マイクロコントローラ向けに async 入出力コードを書くときでも、組み込み向けランタイムの embassy を使えば、ほかの環境で使うコードとほとんど同じ見た目になる。
同じ trait とインターフェースを使うので、ランタイムの詳細をあまり意識しなくて済む。小さな RTOS を使ったり、自前で async 環境を作ったりするのに比べるとかなり良い。
embassy で async コードを書いて学んだことは、ほかの領域にも持っていける。
tokio は標準ライブラリの一部ではないにせよ、きちんと保守されているので、今の状況は悪くないように見える。むしろ標準ライブラリに入ってしまうと、別の executor を使いにくくなり、標準ライブラリを他プラットフォームへ移植するのも難しくなるのではないかと心配している。
もちろん、この心配は杞憂かもしれない。
ロギングは今では slf4j にだいぶ収束したが、いまだに別のものを使うライブラリもあるし、共通ユーティリティは最初は Apache Commons で、今は Guava が多い。
JSON は Jackson にある程度まとまったが、Gson や Simple-json も一般的だし、null 許容性アノテーションも、正式化されなかった JSR-305 の非公式配布版から checker framework を経て、最近では JSpecify へ移行しつつある。
こうした基本要素は言語側が提供しないと、断片化や事実上の標準ライブラリ乱立を避けられない。
ライブラリを executor 非依存で書くのはそれほど難しくないが、継続的な注意が必要で、コミュニティ全体で常に守られているわけではない。
素晴らしい記事だ。こういう最適化の深掘りは好きだし、プロジェクト目標もうまく進んでほしい。
コンパイラは「些細な」ケースの最適化に、しばしばあまり大きな労力を割かないように感じたことがある。
ただ、タイトルは内容に比べて劇的すぎる。「Async Rust Optimizations the Compiler Still Misses」でも十分クリックしたと思う。
いまでは trait やクロージャで async を使えるようになったが、それは型システムの更新であって、async の機械そのものの変化ではない。Waker も少し扱いやすくなったが、どちらかといえば std/core 側の改善に近い。
私の理解では、async Rust を着地させた人たちはかなり燃え尽きて活動が減り、その後を引き継いだ人はほとんどいない。ただ、Google の人たちがキャプチャされた変数のメモリ配置を最適化する PR を 1 本出しているのはかなり嬉しい。
自分と同僚は async を多用しているので、もしかすると自分たちでやるか、少なくとも始めるべきなのかもしれない。「無料」は子犬を飼う意味での無料に近い。
だからタイトルが少し釣り気味なのはその通りだが、それでもそのタイトルを撤回するつもりはない。
著者は些細な関数オーバーヘッドにこだわりすぎているように見える。「panic」と「返却済み」状態のオーバーヘッドを気にしているが、それは大きな問題ではない。
有用な async ブロックの大半は十分に大きいので、エラーケースのオーバーヘッドは埋もれる。
インライン化不足については一理あるかもしれない。ただ、大量のアクティビティ数を制限するのはたいてい各アクティビティが必要とする 状態空間だ。
async は全体として、まだ未熟なアイデアに見える。普通のコードももともと非同期だった。
async な作業を待つ必要があれば、スレッドは準備できるまで眠り、カーネルがそれを抽象化してくれる。ところが論理スレッドとしてコードを構成するのを嫌って、イベント用のコールバックシステムを追加し、その後コールバックは推論しづらく、逐次制御のほうが良いと気づいた。
だから スレッドこそ正しいプログラミングモデルだったのだと思う。
いまや言語ランタイムは移植性と性能のために「グリーンスレッド」を好むが、多くの言語はそれをきちんと提供していない。代わりに async/non-async のカラー問題、スケジューリング、優先度、非プリエンプティブといった問題が出てくる。1970 年代より悪いスケジューリングとプロセスモデルだ。
async コードであっても、表現可能な並行性を最大化しない書き方がよくある。たとえば「N 個の入出力作業を全部同時に実行する」ではなく、「各作業 X について await process(x) する」ような形だ。
しかしスレッドの世界では、この並行性の問題はさらに深刻になる。スレッドは本質的に重すぎて、並行性を効率よく表現しにくく、その方向に最適化する術もない。
これは新しい教訓ではない。ワークスティーリング executor が従来のスレッドよりはるかに低レイテンシで、P99 も安定することは昔から知られていた。2000 年代初頭に Apple が GCD を作った理由もそこにある。
スレッドは、カーネルスケジューラが負荷を理解するのに必要な豊富な情報を渡せず、カーネルスレッドは細粒度の並行性を得るには重すぎる仕組みだ。純計算ではなく、入出力や混合負荷ではなおさら悪い。
すべてのプログラムにこの水準の性能が必要なわけではないが、同じ労力でより高い性能基準を達成するのははるかに容易で、実際、従来のやり方では追いつきにくいレイテンシやスループットが得られる。
async の方向性が正しいことは io_uring にも表れている。カーネルの高性能 I/O 手法である io_uring は、従来のスレッディングやシステムコールとはまったく異なり、完了処理も async 並行性にずっと近い。ただし async/await だけでは async タスク間の関係を表現するにはカラーの数が足りず、完全に活用するのはより難しい。
最後にコルーチン/スケジューリングのコードを触ったときは、すぐ終了するスレッドを作って join するのに約 200µs かかり、自前のグリーンスレッドを作ってスケジューリングして待つのは約 400ns だった。
誰かがまた absurd に複雑な async フレームワークを設計するのを 10 年待つ必要はない。どんなシステム言語でも、アセンブリ 20 行あれば自分で グリーンスレッド/スタック付きコルーチンを作れる。
帯域中心コードの最適化はスケジュール設計の問題だ。古典的なマルチスレッドモデルではスケジューリングを制限付きでしか制御できないが、async モデルではほぼ完全に制御できる。
よく最適化された async スケジュールは、同じ帯域中心タスクにおいて同等のマルチスレッドアーキテクチャよりずっと速く、比べものにならない。
今日の高性能コードの多くは帯域中心であり、async はこうしたワークロードをより最適化しやすくするために存在している。
並行処理をテストして競合状態を適切に処理できるか確認するとき、コールバックはスケジューリングを制御できるのでずっと簡単だ。各コールバックが分離された単位を表すので、どのイベントを並べ替えられるかが見え、さまざまな順序を検討しやすい。
一方スレッドでは順序を無視しやすく、他スレッドで生じる複雑さが、いまのスレッドにいつ影響しうるのかを考えなくなりがちだ。単純なのではなく、単純化されているに近い。
また、人為的なバリアを入れてスレッドを止めたり、I/O をスタブ化して順序を制御するコールバック付きの mock を渡したりしない限り、並行シナリオを実際に変えてテストするのは難しい。
コールバックの問題は、キャプチャされたコールスタックが論理的なコールスタックではないことだ。コールスタックを意味あるものにする努力をした一部のライブラリ/ランタイムでない限り、良いエラー定義が必要になる。
もちろん、2 つのパラダイムを混ぜて両方の欠点だけを得ることもある。
Rust の主目的が安全性なら、なぜ panic があるのか理解できない。コードに panic しうる経路が一切ないことを証明できるべきだ。
今週ずっと調べていたが、決して panic しないことを保証するプログラムを作るのは非常に難しい。自分の理解では panic ハンドラは約 300KB あり、これを除外する唯一の方法は、コンパイル時点でコード内に panic しうる経路がまったく存在しないことだ。コンパイル後のバイナリに panic ハンドラが入っているか確認するやり方はハックっぽく感じる。
unwrap や他の panic 操作を lint で禁止することはできるが、no-panic Rust の部分集合があれば、この記事で扱われた問題のかなりの部分は消えていただろう。
実際にはビット反転でも起きない限り発生しないような状況なのに、理論上 panic しうる演算が多すぎる言語を扱うのはもどかしい。配列が空でないことの証明や async の扱いでも同じだ。
結局、決して起きないはずの状況のために大量のエラー処理を書くか、先頭要素と残りのリストを分ける空でないリスト・パターンのような奇妙な構造を使うことになる。そしてその構造自体も独自の肥大化を持ち込む。
配列が空でないことの証明のようなものも含め、証明ベースの利用を増やす作業もゆっくり進んでいる。
panic がなく、どんな状況でも実行を継続しなければならないなら、不変条件が壊れたメモリ破壊のような状況から復旧しようとして、不変条件を検査するあらゆる場所に大量のエラー処理を書かなければならない。
それは、あなたが懸念しているまさにその問題、つまりほとんど絶対に起きない状況のための膨大なエラー処理とまったく同種のものだ。
ツールがすべてを失敗不能にしてくれるのを望み、自分では何もしたくないという態度にはかなりうんざりする。簡単な API を望み、それで十分簡単でなければ YAML で「プログラミング」する Kubernetes コンテナを望み、それでも足りなければ GCP や Amazon のクリック型ホスティングサービスを望むようになる。
結局それは、プログラミングしたいというより、失敗しないアプリを消費したいだけに近く、そういうライフスタイルは何かを作ってくれる人との共生関係の上にあるだけだ。
こういう醜いが必要な議論は C++ でもしばらく続いてきた。
Rust に async が導入された当初から、その伝染的な性質は好きではなかった。
Rust にはうまくいってほしいし、こういう人たちが増えれば Rust の将来も明るくなるかもしれない。
最近 Rust async の作業を始めたが、いま直面している主な問題は コード重複だ。
非同期 API とブロッキング API の両方をサポートしたい関数は、どれも重複して書かなければならない。
maybe-asyncのようなものがあると良さそうだ。回避策として maybe-async や bisync のような crate を見てみたが、どれも問題や強い制約があった。
asyncやconstのようなキーワードに対して関数をジェネリックにできる キーワードジェネリクス の作業が進んでいる。いま同期/非同期の両方にまたがるコードを書く最善の選択肢は 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
async関数はすでにmaybe-asyncだ。fn -> voidとfn -> Futureの違いは、前者はすぐ最後まで実行され、後者は後になって終わるかもしれないという点だ。async 関数をブロッキング方式で実行したいなら、ブロッキング executor を使えばいい。
この記事が気に入った理由は、2026 年 Rust 目標まで見られたことだ。
チームで Rust を使っているが、必要なことをするためにそこまで深く潜る必要はなかった。それでも、コミュニティからのフィードバックが多い言語が土台から発展していく様子を見るのは楽しい。
C++ ではこういう流れをあまり感じなかったし、ほかの分野でどう回っているのかもよく知らない。
ただ残念なのは、目標ごとに特定の資金調達が必要に見えて、少し Kickstarter 的に感じることだ。今のところ見つかっている最善のモデルがこれなのだろうか。
プロジェクト目標は、ある個人や小さなグループが特定の作業をしたいと表明し、Rust プロジェクトのボランティアにコードレビューや質問への回答など、継続的な支援時間を求める仕組みだ。
それは Rust プロジェクト自体がその目標を立てたとか、必ず支持したという意味ではない。
したがって、これを Rust の公式ロードマップと見るのは適切ではなく、「この領域で作業したい貢献者がいる」程度に見るのがより正確だ。
技術が商業的に定着すると、残念ながらこういう流れになることが多いように思う。大口スポンサーが自分たちの関心ある部分だけを支援するのを責めるのも難しい。
幸い、TweedeGolf のかなりの資金はオランダ政府から来ていると理解している。
新機能は「売る」ことができる。作るのに費用はかかるが、現実の問題を解決し、その問題のコストが機能開発のコストより大きければ、企業はたいてい支払う意思がある。
保守はもっと難しいが、いまはメンテナーファンドもある。RustNL のファンドがその例だ: https://rustnl.org/maintainers/
こうしたファンドは、より広く継続的な作業を対象とし、複数の組織が少しずつ拠出して支える。
これが最善のモデルかは分からないが、少なくともある程度は機能しているようだ。
Rust Async と Tokio のドキュメントを読めば、CPU 集約的な部分を async スタックに入れてはいけない理由、
std::sync::Mutexのような基本ツールを async ブロック内で効率よく使う方法、同期コードと async コードをつなぐ方法がきちんと説明されている。多くのコードは効率に関心がないか、関心があっても必要ないので、こうした指針に従わない。しかし、性能と効率を重視するプロジェクトは多く、コードが本番で動けば落とし穴に気づく。ScyllaDB がその一例だ。
LLM も助けにならない。何もかも
mainまで async にしてしまい、基本ツールの選択を誤り、システムをきちんと設計しない。重複した状態の折りたたみ、つまり
process_commandの例のように match を await 分岐の外へ引き上げるパターンは、今日ある既存の async コードに誰でも適用できる最も簡単な方法だ。コンパイラ作業は不要で、リファクタリングするだけでよい。
「Future は簡単にはインライン化されない」という点について、自分が作ったプログラミング言語では、async 関数内の async 関数呼び出しをインライン化するカスタムパスを書いた。
だいたいうまく動いて、一部のボイラープレートも減らせるが、生成されるバイナリサイズはかなり大きくなる。
技術的には Rust でも同じことはできる。
Lobste.rs の意見
タイトルだけ見て予想していたよりもずっと 建設的な記事 だった
この作業をやろうとしている人が必要な支援を受けられるといいと思う
この問題が扱われているのは良いことだと思う。今の rustc が LLVM にあまりにも多くのコード を渡して、最適化器が全部何とかしてくれることを期待している、という話は何度か見たことがあるが、特にこの記事ではその作業のための 資金支援 まで求めている
しまった、自分は勘違いしていた
async はどんな形であれランタイム、タスク追跡、完了を確認する ポーリング が必要なので、本質的に「重い」ものだとずっと思っていた。オーバーヘッドは 0 ではないのだから
ここで言う「ゼロコスト抽象化」は言語機能の話であって、付随するランタイムとは別だと考えていた
LLVM に渡す前に rustc が何を出力しているのか を見てみようとすら思わなかった
async Rust に慣れていない人向けに言うと:
これは本当にその通り。async 呼び出しが入れ子になったツリーも、最大限に最適化されると、内部に状態機械を持つ 単一の構造体 へと固まる。実に巧妙なやり方だ
リリースビルドでこのケースに到達すると、ある種の デッドロック になるのか? それとも常に
Pendingのタスクを待つタスクのせいでリークが起きる可能性もあるのか?.awaitでは誤ったポーリングはできないいくつか思うところがある:
panic=unwindは好きになれない。一部のテストハーネスを除けば、panic=abortより優れていてコストを相殺できるほどの利点をほとんど見たことがない。テストハーネスですら、Linux ではやや難解なcloneを使ってpthread_joinの代わりに実行スレッドをwaitするような形で、似た選択を適用できそうに思える。この点は自分が間違っているかもしれないリンク、他の人のところでもさっき死んだ?
修正: ブログ記事が 0.5 秒くらい表示されたあと 404 ページ に飛ばされる
修正 2: ブログ記事一覧に入っていろいろクリックしてみたが、一覧にあるその記事を開いても 404 ページに行く。静的ページか、少なくともそうあるべきブログをどうやったらこんなふうに壊せるんだ?
参考までに、同じ再現手順を試してみたようだが、自分の環境では 404 はまったく出なかった。スマホとデスクトップで、JavaScript をオン/オフの両方で試した。だから、起きていた現象は見た目より複雑だったのかもしれない