一个很小的 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 这张,已经到了该认真结算的时候。
