Assembly 使用xor reg,reg是否比mov reg,0具有优势?

Assembly 使用xor reg,reg是否比mov reg,0具有优势?,assembly,x86,micro-optimization,Assembly,X86,Micro Optimization,在x86上,有两种众所周知的方法可以将整数寄存器设置为零值 或者 mov reg, 0 或 有人认为第二种变体更好,因为代码中没有存储值0,这样可以节省生成的机器代码的几个字节。这绝对是好的-使用更少的指令缓存,这有时允许更快的代码执行。许多编译器产生这样的代码 然而,在xor指令和任何更改同一寄存器的早期指令之间存在一种形式上的指令间依赖关系。由于存在依赖性,后一条指令需要等待前一条指令完成,这可能会降低处理器单元的负载并影响性能 add reg, 17 ;do something else

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

或者

mov reg, 0

有人认为第二种变体更好,因为代码中没有存储值0,这样可以节省生成的机器代码的几个字节。这绝对是好的-使用更少的指令缓存,这有时允许更快的代码执行。许多编译器产生这样的代码

然而,在xor指令和任何更改同一寄存器的早期指令之间存在一种形式上的指令间依赖关系。由于存在依赖性,后一条指令需要等待前一条指令完成,这可能会降低处理器单元的负载并影响性能

add reg, 17
;do something else with reg here
xor reg, reg
很明显,无论初始寄存器值是多少,xor的结果都是完全相同的。但处理器是否能够识别这一点

我在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;
}
const int Count=10*1000*1000*1000;
int _tmain(int argc,_TCHAR*argv[]
{
int i;
DWORD start=GetTickCount();
对于(i=0;i

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

我认为在早期的体系结构中,
mov-eax,0
指令通常比
xor-eax,eax
指令花费的时间稍长一些。。。我想不起确切的原因。除非您有更多的
mov
s,否则我认为您不太可能由于代码中存储的一个文本而导致缓存未命中


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

在我卖掉1966年的HR旅行车后,我就无法修理自己的汽车了。我在使用现代CPU时也遇到了类似的问题:-)

这实际上取决于底层的微代码或电路。CPU很有可能识别出
“XOR Rn,Rn”
,并简单地将所有位归零,而不必担心内容。但当然,它可能会对
“MOV Rn,0”
执行相同的操作。一个好的编译器无论如何都会为目标平台选择最佳的变体,所以这通常只是在汇编程序中编码时的问题

如果CPU足够智能,您的
XOR
依赖项将消失,因为它知道该值不相关,并且无论如何都会将其设置为零(这同样取决于实际使用的CPU)


然而,我早已不再关心代码中的几个字节或几个时钟周期了——这看起来像是微优化疯了。

你在写编译器吗

第二,你的基准测试可能不起作用,因为你有一个分支,它可能需要所有的时间。(除非编译器为您展开循环)

无法对循环中的单个指令进行基准测试的另一个原因是,所有代码都将被缓存(与实际代码不同)。因此,通过将mov-eax,0和xor-eax,eax一直缓存在L1中,您已经从图片中消除了很大的大小差异


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

实际答案如下:

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


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

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

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

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

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

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

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


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

我认为这就是我们使用高级语言的原因。如果您真的想知道,只需将codegen阶段更改为执行一个或另一个。基准。选择最好的。啊,老的
xor-reg,reg
trick-过去的好时光:)我认为x86体系结构明确地将xor-reg,reg定义为打破对reg的依赖。请参阅《英特尔体系结构手册》。我希望MOV reg,。。。做同样的事情仅仅因为它是一部电影。因此,您真正的选择是,如果您不关心状态位(XOR会损坏数据),那么哪一个占用更少的空间(我猜执行时间是相同的)
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;
}