将寄存器与自身进行 XOR 是清零的惯用做法,为什么不使用 sub?
Source: Hacker News
为什么使用 xor eax, eax 将寄存器清零
Matt Godbolt,因 Compiler Explorer 而闻名,写了一篇简短的文章,讨论了 x86 编译器为何偏爱 xor eax, eax 指令(原文链接)。
该指令是 x86 上将寄存器置零的最紧凑方式。它比更直观的 mov eax, 0 短几字节,因为它避免了对四字节立即数的编码。x86 架构没有专用的零寄存器,所以必须从头把寄存器清零。
与 sub eax, eax 的比较
xor eax, eax 与 sub eax, eax 编码字节数相同,且在现代 CPU 上的延迟相似,但它们对标志位的影响不同:
| 标志位 | xor eax, eax | sub eax, eax |
|---|---|---|
| OF | 清零 | 清零 |
| SF | 清零 | 清零 |
| ZF | 置位 | 置位 |
| AF | 未定义 | 清零 |
| PF | 置位 | 置位 |
| CF | 清零 | 清零 |
sub eax, eax 会清除 AF 标志,而 xor eax, eax 则让它保持未定义。除这一细微差别外,这两条指令在将寄存器清零的目的上表现相似。
历史与微架构原因
早期的编译器选择 xor 来清零寄存器后,引发了连锁效应:开发者在生成的代码中看到这种模式,便认为它是最佳选择,从而进一步强化了其使用。
Intel 后来在指令译码前端加入了对 xor r, r(以及 sub r, r)模式的特殊检测,将目标寄存器重命名为内部的零寄存器并跳过执行阶段。这使得该指令实际上 零延迟,并且打破了依赖链,因为结果无论输入为何都已知为零。
虽然两种模式都获得了这种特殊处理,但某些 CPU 厂商可能仅对 xor r, r 进行了优化。Stack Overflow 上的讨论指出了这一担忧:How many ways to set a register to zero?。人们普遍认为 xor 在所有平台上都得到优化,这让它在实际使用中更具优势。
额外闲聊
- 我的前同事 Jeff Par 更倾向于使用
sub r, r来清零寄存器。阅读他的汇编代码时,出现sub往往是他的标志。 xor技巧在 Itanium 上不起作用,因为数学运算 不会重置 NaT 位(Old New Thing blog)。幸运的是,Itanium 提供了专用的零寄存器(Old New Thing blog),因此不需要这种技巧。