当然,寄存器与自身异或确实是清零的惯用法,但为何不用减法呢?Sure, xor’ing a register with itself is the idiom for zeroing it out, but why not sub?
尽管异或操作是清零寄存器的常用方法,但作者探讨了为何不采用减法指令。文章分析了不同指令在性能、可读性和硬件实现上的差异,并指出异或之所以流行可能与其在特定架构下的优势有关。
Raymond Chen
马特·戈德博尔(Matt Godbolt),因创立 Compiler Explorer 而闻名,曾撰写一篇短文,解释了为何 x86 编译器如此钟爱 xor eax, eax 这条指令。
答案是:在 x86 架构上,这是将寄存器清零最紧凑的方式。特别是,它比更直观的 mov eax, 0 短几个字节,因为它无需编码四字节常量。x86 没有专用的零寄存器,因此若要将某寄存器清零,必须从头开始构造这个操作。
但马特点明了为何大家选择 xor 而非其他数学运算?毕竟,sub eax, eax 也能保证结果为零。xor 和 sub 编码长度相同,执行周期也一致,甚至在某些方面表现更好。
注意:xor eax, eax 会令 AF(辅助进位)标志位未定义,而 sub eax, eax 则会将其清零。
我不知道 xor 为何最终胜出,但我怀疑这纯属“蜂拥效应”所致。
在我的假设历史中,xor 和 sub 最初流行度相近,但 xor 因某种偶然因素略占上风——也许是因为它看起来更“聪明”。
当早期编译器开始使用 xor 来清零寄存器时,这种用法便形成了滚雪球效应:人们看到编译器生成 xor 指令后心想,“这些编译器真聪明,他们一定掌握了我不知道的技巧。”既然我原本就在 xor 和 sub 之间犹豫不决,这一个微小的数据点就足以让我倒向 xor。
这种清零寄存器的惯用手法大行其道,促使英特尔在指令解码前端加入了特殊的 xor r, r 和 sub r, r 检测机制,并将目标寄存器重命名为内部零寄存器,从而完全绕过指令的实际执行。从这个意义上说,该指令“执行耗时为零个周期”。前端检测还能打破依赖链:通常 xor 或 sub 的输出依赖于其输入,但在这种特殊情况下(寄存器与自身异或或相减),我们已知输出恒为零,与输入无关。
尽管英特尔同时支持 xor-detection 和 sub-detection,Stack Overflow 仍担心其他 CPU 制造商可能只对 xor 做了特殊优化而未处理 sub,这使得 xor 在这场本无意义的竞争中成为赢家。
一旦某条指令获得哪怕极其微弱的优势,就足以扭转局势,吸引众人纷纷转向这一边。
额外闲聊:我的一位前同事偏爱使用 sub r, r 来清零寄存器,因此当我阅读汇编代码时,只要看到他用 sub 而不是更流行的 xor 来清零,就能立刻认出那是他的代码。
再补充一点:xor 技巧在 Itanium 上不适用,因为数学运算不会重置 NaT 位。不过幸运的是,Itanium 本身就有专用零寄存器,所以你根本不需要这个技巧,直接 mov 零值到目标寄存器即可。
分类
主题
作者
雷蒙德从事 Windows 相关开发已超过三十年。2003 年,他创办了名为《旧新事物》(The Old New Thing)的网站,其受欢迎程度远超他本人当初的想象,至今仍让他感到些许不安。该网站催生了一本同名书籍《旧新事物》(Addison Wesley, 2007)。他偶尔会在 Windows Dev Docs 的 Twitter 账号上出现,讲述一些毫无实用价值的故事。
需要完整排版与评论请前往来源站点阅读。