58

有两种众所周知的方法可以在 x86 上将整数寄存器设置为零值。

任何一个

mov reg, 0

或者

xor reg, reg

有一种观点认为第二种变体更好,因为值 0 没有存储在代码中,并且节省了生成的机器代码的几个字节。这绝对是好的——使用更少的指令缓存,这有时可以加快代码执行速度。许多编译器产生这样的代码。

然而,在 xor 指令和任何更改同一寄存器的早期指令之间存在正式的指令间依赖关系。由于存在依赖性,后一条指令需要等到前一条指令完成,这可能会减少处理器单元的负载并损害性能。

add reg, 17
;do something else with reg here
xor reg, reg

很明显,无论初始寄存器值如何,异或的结果都将完全相同。但它的处理器能够识别这一点吗?

我在 VC++7 中尝试了以下测试:

const int Count = 10 * 1000 * 1000 * 1000;
int _tmain(int argc, _TCHAR* argv[])
{
    int i;
    DWORD start = GetTickCount();
    for( i = 0; i < Count ; i++ ) {
        __asm {
            mov eax, 10
            xor eax, eax
        };
    }
    DWORD diff = GetTickCount() - start;
    start = GetTickCount();
    for( i = 0; i < Count ; i++ ) {
        __asm {
            mov eax, 10
            mov eax, 0
        };
    }
    diff = GetTickCount() - start;
    return 0;
}

关闭优化后,两个循环的时间完全相同。这是否合理地证明处理器认识到指令不依赖xor reg, reg于先前的mov eax, 0指令?有什么更好的测试来检查这一点?

4

6 回答 6

33

给你一个实际的答案:

Intel 64 和 IA-32 架构优化参考手册

第 3.5.1.8 节是您要查看的地方。

简而言之,在某些情况下,可能首选 xor 或 mov。问题集中在依赖链和条件代码的保存上。

于 2009-07-16T07:31:17.303 回答
15

在现代 CPU 上,XOR 模式是首选。它更小,更快。

较小实际上确实很重要,因为在许多实际工作负载中,限制性能的主要因素之一是 i-cache 未命中。这不会在比较这两个选项的微基准测试中被捕获,但在现实世界中它会使代码运行得稍微快一些。

而且,忽略减少的 i-cache 未命中,过去许多年任何 CPU 上的 XOR 速度都与 MOV 相同或更快。有什么比执行 MOV 指令更快的呢?根本不执行任何指令!在最近的英特尔处理器上,分派/重命名逻辑识别 XOR 模式,“意识到”结果将为零,并将寄存器指向物理零寄存器。然后它会丢弃该指令,因为不需要执行它。

最终结果是 XOR 模式使用零执行资源,并且在最近的 Intel CPU 上,每个周期可以“执行”四条指令。MOV 在每个周期最多执行三个指令。

有关详细信息,请参阅我写的这篇博文:

https://randomascii.wordpress.com/2012/12/29/the-surprising-subtleties-of-zeroing-a-register/

大多数程序员不应该担心这一点,但编译器编写者确实需要担心,理解正在生成的代码是件好事,这简直太酷了!

于 2015-03-19T23:28:26.180 回答
13

x86 具有可变长度指令。MOV EAX, 0 在代码空间中比 XOR EAX, EAX 需要多一到两个字节。

于 2009-10-15T13:42:30.707 回答
12

在我卖掉 1966 年 HR 旅行车后,我不再能够修理自己的汽车。我正在使用现代 CPU 进行类似的修复:-)

它实际上将取决于底层的微码或电路。CPU很可能可以识别"XOR Rn,Rn"所有位并将其简单地归零而无需担心内容。但当然,它可能对"MOV Rn, 0". 无论如何,一个好的编译器都会为目标平台选择最好的变体,所以这通常只有在您使用汇编程序编码时才会成为问题。

如果 CPU 足够聪明,您的XOR依赖就会消失,因为它知道该值无关紧要,并且无论如何都会将其设置为零(这再次取决于实际使用的 CPU)。

但是,我早已不再关心代码中的几个字节或几个时钟周期——这似乎是微优化发疯了。

于 2009-07-16T06:14:28.193 回答
2

我认为在早期的体系结构中,mov eax, 0指令过去花费的时间xor eax, eax也比之前的要长……无法准确回忆起原因。除非您有更多movs 但是我想您不太可能由于代码中存储的一个文字而导致缓存未命中。

另请注意,从记忆中,这些方法之间的标志状态并不相同,但我可能记错了。

于 2009-07-16T06:10:58.793 回答
-7

你在写编译器吗?

其次,您的基准测试可能不起作用,因为您在那里有一个分支,无论如何可能都需要花费所有时间。(除非您的编译器为您展开循环)

您无法在循环中对单个指令进行基准测试的另一个原因是您的所有代码都将被缓存(与真实代码不同)。因此,通过将 mov eax,0 和 xor eax,eax 始终置于 L1 缓存中,您已将其大部分大小差异排除在外。

我的猜测是,现实世界中任何可测量的性能差异都是由于大小差异占用了缓​​存,而不是由于两个选项的执行时间。

于 2009-07-16T06:48:50.073 回答