返回 2026-04-21
⚙️ 工程

为何用 XOR 清零寄存器而非 SUB?Sure, xor’ing a register with itself is the idiom for zeroing it out, but why not sub?

尽管 SUB 指令同样可将寄存器设为零,XOR reg, reg 成为主流清零惯用语,原因包括更短的操作码、不依赖标志位输入、以及早期处理器上的性能优势。历史演变和微架构优化共同促成了这一看似反直觉但高效的选择。文章追溯了 x86 架构中该惯用语的起源与持续影响。

Raymond Chen

Matt Godbolt 因创办 Compiler Explorer 而广为人知,他写了一篇短文,探讨为何 x86 编译器偏爱 xor eax, eax 这条指令。

原因在于,这是 x86 架构上将寄存器清零最紧凑的方式。具体来说,它比更直观的 mov eax, 0 短几个字节,因为后者需要编码一个四字节的常量。x86 架构没有专用的零寄存器,因此若要将寄存器清零,必须从头开始操作。

但 Matt 并未解释为何大家都选择 xor,而不是其他同样能保证结果为零的数学运算?特别是,sub eax, eax 有什么问题?它的编码长度相同,执行周期数也一样。而且它对标志位的影响甚至更优:

注意,xor eax, eax 会使 AF 标志位处于未定义状态,而 sub eax, eax 则会将其清零。

我不知道为何 xor 最终胜出,但我怀疑这只是“群聚效应”的结果。

在我的假想历史中,xor 和 sub 最初 popularity 大致相当,但 xor 因某种偶然因素略微领先,也许是因为它看起来更“巧妙”。

早期编译器使用 xor 清零寄存器时,便开启了滚雪球效应:人们看到编译器生成 xor 指令,会想:“嗯,这些编译器作者很聪明,他们肯定知道一些我不知道的东西。既然我在 xor 和 sub 之间犹豫不决,这个微小的数据点就足以让我倒向 xor。”

这些将寄存器清零的惯用写法盛行后,Intel 在指令解码前端加入了特殊的 xor r, r 检测和 sub r, r 检测机制,并将目标寄存器重命名为一个内部零寄存器,从而完全绕过指令的执行。你可以认为,这条指令“在某种意义上执行时间为零周期”。前端检测还打破了依赖链:通常,xor 或 sub 的输出依赖于其输入,但在这种将寄存器与自身进行 xor 或 sub 的特殊情况下,我们知道输出必然为零,与输入无关。

尽管 Intel 同时支持 xor 检测和 sub 检测,但 Stack Overflow 担心其他 CPU 制造商可能只对 xor 做了特殊优化,而未对 sub 做同样处理,这使得 xor 在这场最终毫无意义的竞争中胜出。

一旦某条指令获得哪怕极其微小的优势,也足以打破平衡,促使所有人倒向那一方。

额外闲聊:我的一位前同事偏爱使用 sub r, r 来清零寄存器,因此当我阅读汇编代码时,只要看到用 sub 而非更常见的 xor 来清零寄存器,就能判断出是他写的。

额外额外闲聊:xor 技巧在 Itanium 上无效,因为数学运算不会重置 NaT 位。幸运的是,Itanium 本身就有专用的零寄存器,因此你根本不需要这种技巧,只需将零移动到目标寄存器即可。

分类

主题

作者

Raymond 参与 Windows 的演进已超过 30 年。2003 年,他创办了一个名为 The Old New Thing 的网站,其受欢迎程度远远超出了他最疯狂的想象,这一发展至今仍让他感到心惊肉跳。该网站催生了一本书,巧合的是,书名也叫《The Old New Thing》(Addison Wesley,2007 年)。他偶尔会出现在 Windows Dev Docs 的 Twitter 账号上,讲述一些毫无实用价值的故事。

需要完整排版与评论请前往来源站点阅读。