News Hacker|极客洞察

33 184 天前 matklad.github.io
🤔错误表示、ABI 与隐式栈展开之争
要把显式的错误返回偷偷变成隐式的 unwind 吗?

🎯 讨论背景

讨论起于一篇提出改变错误表示与函数调用约定的技术文章,核心建议是在静态链接与 LTO 场景下让编译器为函数推导更合适的 ABI,并用侧表+stack unwinding 的方式把错误返回映射到恢复地址。评论分为主张优化的观点和担忧语义/可实现性的观点:前者强调可减少拷贝与寄存器抖动、降低代码体积;后者担心这会等同于以另一种名义引入 exceptions 并增加分析复杂性。讨论还涉及语言生态差异:有人把焦点放在 Zig(一个系统编程语言)而非 Rust(另一个系统编程语言),也有人提醒 Rust 已有 panic=unwind 与 catch_unwind,且 Rustonomicon 对常态使用 unwinding 有明确警告。另有评论提到 Linux kernel 在错误处理策略上也出现相关变动,表明该议题在更广生态中被关注。

📌 讨论焦点

按用法推导的灵活 ABI(静态链接、LTO)

有人主张在静态链接的情形下不必受限于僵化的 ABI,编译器可以在 LTO(Link Time Optimization)阶段根据函数在上下文中的实际用法为每个函数推导专用 ABI,以最小化不必要的拷贝和寄存器抖动。该观点认为,当前为了满足通用 ABI 而放弃的代码体积和性能优化很多,按用法定制 ABI 是 LTO 的自然下一步。实现上假定编译器和链接器能跨模块看到调用模式并重写调用约定,从而把“为了兼容而牺牲优化”的成本移除。

[来源1]

质疑与反驳:性能瓶颈不在 ABI,偏离现有 ABI 成本高

反对者指出绝大多数现实中遇到的性能问题与 ABI 无关,优化点往往在算法、数据结构或更高层的系统设计。评论强调现代 ABI 与硬件是在长期演进中相互配合、被高度优化的——这形成了一条“高速公路”,一旦脱离就会迅速遭遇性能回归。因而在实际系统中偏离通用 ABI 会带来额外复杂性和不可预期的开销,未必能换来明显的收益。

[来源1] [来源2]

担忧:把错误表示变成类似异常/隐式 unwinding 会破坏显式错误语义

有评论担心把错误的二进制表示变成通过侧表查找并跳转到恢复地址的模式,本质上是以另一种名义引入 exceptions/stack unwinding。批评者认为这种“别名异常”违背像 Rust 一类语言强调显式错误处理的设计意图,同时把控制流语义隐化会增加 API 与抽象层次的混乱。文章中声称“unwinding 是最优”的断言被指责为过于武断;实现复杂度也被提出:编译器必须识别哪些代码位于错误路径,而每个函数可能有多条不同的错误路径,这会让静态分析和优化变得棘手并令 Result 更加“特殊”。

[来源1] [来源2] [来源3] [来源4]

澄清与现有机制:不是语言级异常,Rust 已有 panic=unwind,使用受限

另有评论澄清文章主要讨论的是错误的二进制表示(representation)而非在语言层面新增 throw/catch 式语法,提议是借鉴有 exceptions 的语言和 Rust 自身的 panic=unwind 来实现现有的错误处理语义而不改语言。实现上栈只有在错误向上冒泡且需要恢复时才会展开,错误也可以在栈上被丢弃以阻止继续展开,但为了做到这一点编译器需要精确判断错误路径并处理每条不同的错误流。评论还指出 Rust 已经支持 panic=unwind 并提供 catch_unwind,但 Rustonomicon 警告称常态使用 unwinding 成本高——实现对“不会展开”路径做了优化,实际展开会比 Java 等语言更昂贵,因此不应把 unwind 当成常规错误路径。另有人提到 Linux kernel 在错误处理模式上也出现类似压力,说明这类方案在更广范围内被讨论。

[来源1] [来源2] [来源3] [来源4] [来源5] [来源6]

📚 术语解释

ABI: Application Binary Interface,定义函数调用约定、参数传递、寄存器和栈布局等二进制约定,直接影响二进制兼容性、性能和代码大小。

LTO: Link Time Optimization(链接时优化),编译器在链接阶段进行跨模块分析与优化,使按用法定制 ABI 与跨模块内联等全程序级优化成为可能。

stack unwinding: 栈展开/栈展开机制,一种错误或异常传播方式:通过查找恢复地址并跳转(同时可能执行析构/清理)来传播错误。讨论中该术语用来描述通过侧表查找错误恢复地址的策略。

panic=unwind: Rust 的构建配置选项,启用时 panic 会以栈展开的方式传播,配合 catch_unwind 可被捕获,但社区倾向仅在极端或互操作场景使用,因为展开成本较高。

Result: Rust 中的标准错误返回类型,表示成功(T)或错误(E)。讨论中提到改变错误的二进制表示可能会让 Result 在实现上变得更加“特殊”或隐性。