错误 ABI
Error ABI

原始链接: https://matklad.github.io/2025/11/09/error-ABI.html

## 优化错误处理性能 普遍认为使用代数数据类型 (ADT) 处理错误是一种“零成本抽象”,这种观点是有缺陷的。虽然错误很少发生,但未经优化的使用 ADT 组合错误可能会*降低*成功代码路径(“快乐路径”)的性能。大型错误对象会膨胀 `Result` 类型的大小,导致函数通过指针返回大型结构体,从而导致“错误病毒”——影响所有地方的性能。 成熟的库通过错误指针来解决这个问题,但这仍然依赖于全局分配器,会产生开销。更好的方法是优化 `Result` 的应用程序二进制接口 (ABI)。选项包括为错误保留一个寄存器(如果错误大小适合寄存器),或者更激进地,在发生错误时利用堆栈展开而不是标准返回值。 核心思想是将可见的编程模型与内部实现解耦,从而可以有效地实现 `Result`——甚至可以通过返回值检查模拟异常处理。最终,最佳策略取决于语言的抽象能力,抽象能力较弱的语言将受益于一流的错误语义。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Error ABI (matklad.github.io) 12 分,by todsacerdoti 1 小时前 | 隐藏 | 过去 | 收藏 | 讨论 考虑申请YC冬季2026批次!申请截止至11月10日 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请YC | 联系 搜索:
相关文章

原文

A follow-up on the “strongly typed error codes” article.

One common argument about using algebraic data types for errors is that:

  1. Error information is only filled in when an error occurs,
  2. And errors happen rarely, on the cold path,
  3. Therefore, filling in the diagnostic information is essentially free, a zero cost abstraction.

This argument is not entirely correct. Naively composing errors out of ADTs does pessimize the happy path. Error objects recursively composed out of enums tend to be big, which inflates size_of<Result<T, E>>, which pushes functions throughout the call stack to “return large structs through memory” ABI. Error virality is key here — just a single large error on however rare code path leads to worse code everywhere.

That is the reason why mature error handling libraries hide the error behind a thin pointer, approached pioneered in Rust by failure and deployed across the ecosystem in anyhow. But this requires global allocator, which is also not entirely zero cost.

How would you even return a result? The default option is to treat -> Result<T, E> as any other user-defined data type: goes to registers if small, goes to the stack memory if large. As described above, this is suboptimal, as it spills small hot values to memory because of large cold errors.

A smarter way to do this is to say that the ABI of -> Result<T, E> is exactly the same as T, except that a single register is reserved for E (this requires the errors to be register-sized). On architectures with status flags, one can even signal a presence of error via, e.g., the overflow flag.

Finally, another option is to say that -> Result<T, E> behaves exactly as -> T ABI-wise, no error affordances whatsoever. Instead, when returning an error, rather than jumping to the return address, we look it up in the side table to find a corresponding error recovery address, and jump to that. Stack unwinding!

The bold claim is that unwinding is the optimal thing to do! I don’t know of a good set of reproducible benchmarks, but I find these two sources believable:

As with async, keep visible programming model and internal implementation details separate! Result<T, E> can be implemented via stack unwinding, and exceptions can be implemented via checking the return value.

Your error ABI probably wants to be special, so the compiler needs to know about errors. If your language is exceptional in supporting flexible user-defined types and control flow, you probably want to special case only in the backend, and otherwise use a plain user-defined type. If your language is at most medium in abstraction capabilities, it probably makes sense to make errors first-class in the surface semantics as well.

联系我们 contact @ memedata.com