为什么是 xor eax, eax? 作者:我(本人撰写),由大语言模型校对。 细节见文末。
在我的一次关于汇编语言的演讲中,我展示了一份平均x86 Linux桌面上执行次数最多的20条指令列表。所有常见的指令都在那里,mov、add、lea、sub、jmp、call等等,但令人惊讶的闯入者是xor——“异或”。在我捣鼓6502处理器的日子里,异或指令的存在是明确无误的信号,表明你找到了代码的加密部分,或者某种精灵处理例程。因此,一台正常运行的Linux机器竟然会执行这么多xor指令,这令人意外。
直到你想起,编译器在将寄存器设置为零时,喜欢生成xor指令:
|
|
我们使用异或将任何值与自身进行异或运算会得到零,但为什么编译器要生成这条指令呢?它只是在炫耀吗?
在上面的例子中,我使用了-O2优化级别进行编译,并启用了Compiler Explorer的“编译为二进制对象”选项,以便你可以查看CPU看到的机器代码,具体是:
|
|
如果你将GCC的优化级别降低到-O1,你会看到:
|
|
更清晰、更能揭示意图的mov eax, 0指令将EAX寄存器设置为零,但它占用了五个字节,而异或指令只占用两个字节。通过使用一个稍微晦涩一点的指令,每次我们需要将寄存器清零时(这是一个非常常见的操作),我们节省了三个字节。节省字节可以使程序更小,并更有效地利用指令缓存。
但这还不是全部!由于这是一个非常常见的操作,x86 CPU在流水线早期就能识别出这种“清零习惯用法”,并可以专门围绕它进行优化:乱序执行跟踪系统知道“eax”(或任何被清零的寄存器)的值不依赖于eax之前的值,因此它可以分配一个新的、无依赖关系的零寄存器重命名槽。完成此操作后,它将该操作从执行队列中移除——也就是说,xor指令的消耗为零个执行周期!它基本上被CPU优化掉了!
你可能会好奇,为什么你看到的是xor eax, eax,却从未见过xor rax, rax(64位版本),即使在返回一个long类型时:
|
|
在这个例子中,尽管rax需要容纳完整的64位long结果,但通过对eax进行写操作,我们得到了一个很好的效果:与其他部分寄存器写入不同,当写入像eax这样的“e”寄存器时,架构会自动将高32位清零。因此,xor eax, eax将全部64位都设为零。
有趣的是,在清零“扩展”编号寄存器(如r8)时,GCC仍然使用d(双字宽,即32位)变体:
|
|
注意看,它是xor r8d, r8d(32位变体),即使使用REX前缀(这里是45),对完整的r8寄存器进行异或(xor r8, r8)也占用相同的字节数。这可能让编译器内部的某些处理更容易,因为clang编译器也这么做。
xor eax, eax节省了你的代码空间和执行时间!感谢编译器!
观看随本文发布的视频。
本文是《2025年编译器优化降临历》的第一天内容,这是一个为期25天的系列,探讨编译器如何转换我们的代码。
本文由人类(Matt Godbolt)撰写,并经过大语言模型和人类审查校对。
在Patreon或GitHub上支持Compiler Explorer,或通过Compiler Explorer商店购买CE产品。