【问题标题】:Does using xor reg, reg give advantage over mov reg, 0? [duplicate]使用 xor reg, reg 是否比 mov reg, 0 更有优势? [复制]
【发布时间】:2010-11-11 06:19:31
【问题描述】:

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

要么

mov reg, 0

xor reg, reg

有一种观点认为第二种变体更好,因为值 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;
}

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

【问题讨论】:

  • 我认为这就是我们使用高级语言的原因。如果您真的想知道,只需将 codegen 阶段更改为做一个或另一个。基准。选择最好的。
  • 啊,老 xor reg, reg 把戏 - 美好的旧时光 :)
  • 我认为 x86 架构明确定义 XOR reg,reg 以打破对 reg 的依赖。请参阅英特尔架构手册。我希望 MOV reg,... 做同样的事情仅仅是因为它是一个 MOV。所以你真正的选择是,如果你不关心状态位(异或会损坏它们),哪一个占用更少的空间(我猜执行时间是一样的)。
  • 您的 Count 变量溢出,因此循环运行的周期比您预期的要少得多
  • 在最近的微架构上,xor reg,reg 不需要执行单元(在解码中处理?)。它打破了对reg 的依赖,并且部分标志更新停止。而且它的编码更小。在最近的 x86-64 上使用 mov 方法没有充分的理由,除非您必须保留 [e] 标志。

标签: assembly x86 micro-optimization


【解决方案1】:

给你一个实际的答案:

Intel 64 and IA-32 Architectures Optimization Reference Manual

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

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

【讨论】:

  • 这听起来不像引用的文字建议在任何情况下都使用 MOV。
  • @mwfearnley 不幸的是,Addison 决定编辑我的答案并挑选一部分内容,目前尚不清楚为什么要这样做。您应该阅读完整的文档,其中涵盖首选 mov 的情况。
  • 感谢您的澄清。我想这是为了避免文档移动/更改问题的尝试,但不幸的是,引用并没有包含它需要的所有要点。我现在可以从那部分看到,它说当你想避免时使用 MOV设置条件代码。
  • @mwfearnley:在设置标志之前你不能只是异或零是很少见的。有关避免mov reg, 0 以准备setcc 的一些建议,请参阅my answer on the more recent xor question。 (以及有关异或归零所有优点的更多详细信息)。 mov reg,0 / setcc 在旧的 Intel CPU 上很糟糕,读取完整的 reg 会导致部分寄存器停顿,xor 可以避免。
【解决方案2】:

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

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

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

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

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

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

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

【讨论】:

  • 好文章!我想知道 Thumb 上是否存在相同的模式。
  • Thumb 上很可能存在相同的优化。该优化适用于任何乱序处理器,应该可以节省电力,有时还可以提高性能。但是,我不知道。
【解决方案3】:

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

【讨论】:

  • mov eax, 0 是 5 个字节:一个用于mov eax, imm32 操作码,4 个用于 4B 立即数数据。 xor eax, eax 是 2 个字节:一个 xor r32, r/m32 操作码,一个用于操作数。
【解决方案4】:

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

这真的取决于底层的微码或电路。 CPU 很可能可以识别"XOR Rn,Rn" 并简单地将所有位归零而不用担心内容。但当然,它可能对"MOV Rn, 0" 做同样的事情。一个好的编译器无论如何都会为目标平台选择最好的变体,所以这通常只有在你用汇编程序编码时才会出现。

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

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

【讨论】:

  • 无论是否为实际使用过度优化,理解并非所有类似指令都是平等的可能是有价值的。 ;)
  • @jerryjvl - 意识到现代桌面 x86 CPU 不运行 x86 机器代码也很有用 - 它们将 x86 解码为 RISC 类似内部指令来执行。因此,它们可以识别常见的代码序列(如 xor eax、eax)并将它们转换为更简单的指令,例如一些“clear reg”指令。在这种情况下,可能不会进行实际的异或。
  • 在编写 MBR 时可能需要进行微优化 =)。
  • @sh1ftst0rm : 现在只有不聪明的人才会做这种事。
【解决方案5】:

我认为在早期的架构中,mov eax, 0 指令过去也比xor eax, eax 花费的时间要长一点……不记得确切的原因。除非您有更多 movs,否则我想您不太可能由于代码中存储的一个文字而导致缓存未命中。

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

【讨论】:

    【解决方案6】:

    你在写编译器吗?

    另外,您的基准测试可能无法正常工作,因为您在那里有一个分支,无论如何可能都需要花费所有时间。 (除非你的编译器为你展开循环)

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

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

    【讨论】:

    • 整个网站对世界其他地方都具有“谁在乎”的品质。我认为这不是一个好的答案。
    • 您和其他人似乎都在关注我认为您认为令人反感的内容。我已删除该部分,因为我认为您和其他人从未阅读过该部分并且只是投反对票。
    • 对于 Sandybridge / Ivybridge,您可以很容易地使用nopxor same,same 构建一个以每时钟1 次迭代运行的循环,但使用mov reg,0 会成为ALU 执行单元吞吐量的瓶颈。后来的 Intel CPU 有 4 个 ALU 执行单元,因此构建除代码大小之外的可测量差异的异或零消除的具体示例要容易得多。 (xorps xmm/ymm regs 的归零仍然很容易,因为矢量 ALU 端口比前端宽度少)。而且 AMD CPU 不会消除后端 uop,因此优势实际上只是代码大小。
    • 大多数代码在大多数情况下都会获得 L1i 缓存命中。 L1i 缓存未命中确实会发生,但在程序过程中执行的大部分指令确实来自 L1i 缓存,甚至是更小/更快的 uop 缓存。大多数程序将大量时间花在中小型循环上。缓存工作。
    • 你说得对,虽然 OP 的基准测试不太可能奏效。但它可能在 Sandybridge 上,如果循环开销是 2 个额外的 ALU 微指令,则总共有 4 个前端微指令。如果其中一个是可以消除的异或归零,后端可以处理。
    猜你喜欢
    • 2016-02-16
    • 1970-01-01
    • 1970-01-01
    • 2012-09-15
    • 2021-05-06
    • 1970-01-01
    相关资源
    最近更新 更多