一个很小的 async 例子,最后可能变成几百行 MIR。更刺眼的是,async 块里哪怕没有 await,Rust 编译器也可能照样给它生成状态机。

Tweede golf 的工程师最近把这件事摊开讲了。他的判断很直接:Async Rust 仍像停在 MVP 状态。不是不能用,也不是失败,而是编译器还没把该吃掉的成本吃掉。

他已经提交了一个 Rust Project Goal,希望寻求资金和协作,在 rustc 里处理 async bloat。这里要划清边界:这不是 Rust 官方已经拍板的路线,而是一个提案和工程目标。

这件事最该被嵌入式、Wasm、体积敏感服务团队看见。如果你的项目大量使用 async trait、分层协议、executor 无关抽象,也别太快把二进制膨胀归咎于“自己写法不够克制”。

膨胀从 Future 状态机开始

Async Rust 会在 MIR 阶段被降成 Future 状态机。一个简单 Future,也可能包含 Unresumed、Returned、Panicked、Suspend 等状态。

有 await,就有挂起点。完成后进入 Returned。panic 后进入 Panicked。重复 poll 时,编译器还要守住安全边界。

这些设计有理由。Future::poll 是安全函数,不能让重复 poll 或 panic 后再 poll 变成 UB。

但安全不是免费午餐。成本会落到状态、分支、调用路径和二进制体积里。

问题点发生了什么最痛的项目
Returned 状态Future 完成后再次 poll 触发 panic 路径嵌入式、Wasm、size 优化目标
无 await async仍可能生成状态机小固件、小工具链目标
Future 嵌套状态机套状态机,层层包裹async trait、协议栈、抽象层多的项目
重复 await 分支相似状态没有被折叠命令处理、网络服务、设备协议
依赖 LLVM 兜底简单场景能救,复杂场景不稳定opt-size、嵌入式、Wasm

作者做了两个 hack 级实验。

一个是把 Returned 状态下的 panic 改为返回 Pending。在部分嵌入式固件里,二进制体积约省 2%-5%。

另一个是 async 块没有 await 时不生成状态机。收益约 0.2%。

两者叠加后,在使用 smol executor 的 x86 合成基准里,约有 3% 性能提升。

这些数字不能乱外推。它们来自作者的实验补丁和部分场景,不代表整个 Rust 生态都能拿到同样收益。

但它至少说明一件事:这里有真实的编译器债务。不是少数开发者“感觉代码变胖了”。

LLVM 救不了所有 async 代码

常见反应是:MIR 啰嗦一点没关系,LLVM 后面会优化。

简单场景里,确实可能。问题是 Async Rust 的惯用写法很容易堆出深层 Future 嵌套。

一旦进入 async trait、分层协议、通用 executor、抽象适配器,状态机就不是一层。LLVM 需要看穿更多调用、分支和状态。

在追求体积的构建里,这件事更难。optimize for size、嵌入式目标、Wasm 目标,都不能指望 LLVM 稳定把这些东西折干净。

还有一个硬约束:panic 路径有语义。编译器不能随便假设“这个 Future 只会被正确 poll 一次”。

作者提到的优化方向并不玄:

  • release 模式下,Returned 状态不再 panic,而是返回 Pending;
  • async 块没有 await,就不生成状态机;
  • 对单 await 的 Future 做内联,避免 bar 状态机外面再包一层 foo 状态机;
  • 合并重复状态,比如 match 两个分支里 await 同一个 send_response。

但第一条不能说成“删掉一个 panic 就完事”。它会改变 release 行为,牵涉 Future 合约、调试体验和 executor 合规性。

这就是现实约束。编译器优化不是扫垃圾。它动的是语言承诺和生态默认行为。

对团队来说,短期动作也很具体。

做嵌入式或 Wasm 的团队,不必因为这篇文章立刻迁移技术栈。但如果固件体积、Wasm 包体积、冷启动已经卡线,就该把 async 状态机纳入排查项。

可以先做三件事:看 release 体积变化,看 async 抽象层是否过深,看关键路径里是否有无 await 的 async 包装和重复 await 分支。

如果项目正准备大规模引入 async trait,尤其是在体积敏感目标上,建议先做小样本基准。别等抽象铺满以后,再靠手工拆函数还债。

真正该还的是零成本抽象

我不太买账的一种说法是:开发者写得克制一点就行。

这话在小项目里成立。在生态里不成立。

Rust 鼓励抽象。async trait、分层协议、跨 executor 代码,本来就是它吸引开发者的地方。

结果抽象一多,状态机膨胀的账让嵌入式和 Wasm 开发者自己拆函数、重排 match、绕开 panic 路径。这个分工不对。

“天下熙熙,皆为利来。”放到技术生态里,利不只是钱,也是心智成本。

语言许诺零成本抽象,开发者就会放心叠抽象。编译器没兑现,成本就会从机器码、固件大小、冷启动和调试复杂度里冒出来。

这篇文章有意思的地方,不是骂 Async Rust。作者的立场恰恰是喜欢 Async Rust。

它让同一套并发模型可以进入服务器、Wasm 和微控制器。这件事本身很难得。

但喜欢不等于替它遮账。

接下来最该观察的,不是某个 benchmark 又快了几个百分点。而是这个 Project Goal 能不能变成可投入的 rustc 工作:有没有资金、有没有维护者时间、语义边界怎么定、哪些优化能先落地。

Returned 后返回 Pending 这种改动,尤其要看讨论结果。它有体积收益,也有行为代价。

Async Rust 的问题不在“能不能用”。它早就能用。

真正的分水岭是:继续把 bloat 当成开发者写法问题,还是承认它已经是编译器层面的生态成本。

前者会让高手继续手工优化,普通项目继续踩坑。后者慢,也难,但方向更对。

零成本抽象不是口号,是欠条。async 这张,已经到了该认真结算的时候。