零分配与xor,第二个真的更快?

std*_*all 16 assembly compilation

有人在几年前向我展示了以下命令将变量归零.

xor i,i
Run Code Online (Sandbox Code Playgroud)

他告诉我,这比为它分配零要快.这是真的吗?编译器是否进行优化以使代码执行此类操作?

old*_*mer 28

你可以自己试试看看答案:

  movl $0,%eax
  xor %eax,%eax
Run Code Online (Sandbox Code Playgroud)

组装然后拆卸:

as xor.s -o xor.o
objdump -D xor.o
Run Code Online (Sandbox Code Playgroud)

得到

   0:   b8 00 00 00 00          mov    $0x0,%eax
   5:   31 c0                   xor    %eax,%eax
Run Code Online (Sandbox Code Playgroud)

32位寄存器的mov指令是2.5倍,从ram加载需要更长的时间并消耗更多的缓存空间.在当天加载时间本身就是一个杀手,今天内存周期时间和缓存空间可能被认为不那么引人注目,但如果你的编译器和/或代码经常这样做,你会发现丢失缓存空间和/或更多的驱逐,以及更慢,系统内存周期.

在现代CPU中,较大的代码大小也会降低解码器的速度,可能会阻止它们在每个周期解码其最大数量的x86指令.(例如,某些CPU的16B块中最多4条指令.)

在某些与代码大小无关的x86 CPU(尤其是Intel)中,xor over mov也有性能优势,因此在x86汇编中始终首选xor-zeroing.


另一组实验:

void fun1 ( unsigned int *a )
{
    *a=0;
}
unsigned int fun2 ( unsigned int *a, unsigned int *b )
{
    return(*a^*b);
}
unsigned int fun3 ( unsigned int a, unsigned int b )
{
    return(a^b);
}


0000000000000000 <fun1>:
   0:   c7 07 00 00 00 00       movl   $0x0,(%rdi)
   6:   c3                      retq   
   7:   66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
   e:   00 00 

0000000000000010 <fun2>:
  10:   8b 06                   mov    (%rsi),%eax
  12:   33 07                   xor    (%rdi),%eax
  14:   c3                      retq   
  15:   66 66 2e 0f 1f 84 00    nopw   %cs:0x0(%rax,%rax,1)
  1c:   00 00 00 00 

0000000000000020 <fun3>:
  20:   89 f0                   mov    %esi,%eax
  22:   31 f8                   xor    %edi,%eax
  24:   c3                      retq   
Run Code Online (Sandbox Code Playgroud)

沿着显示变量xor i的路径向前迈进,我在你的问题中可能会导致.由于您没有指定您所指的处理器或上下文,因此难以描绘整个画面.例如,如果您正在讨论C代码,您必须了解编译器对该代码执行的操作,并且这在很大程度上取决于函数本身的代码,如果在xor时编译器在寄存器中具有操作数并依赖于在您的编译器设置上,您可能会获得xor eax,eax.或者编译器可以选择将其更改为mov reg,0,或更改某些内容= 0; 到xor reg,reg.

还有一些要思考的序列:

如果变量的地址已经在寄存器中:

   7:   c7 07 00 00 00 00       movl   $0x0,(%rdi)

   d:   8b 07                   mov    (%rdi),%eax
   f:   31 c0                   xor    %eax,%eax
  11:   89 07                   mov    %eax,(%rdi)
Run Code Online (Sandbox Code Playgroud)

编译器将选择mov零而不是xor.如果您尝试使用此C代码,您将获得以下内容:

void funx ( unsigned int *a )
{
    *a=*a^*a;
}
Run Code Online (Sandbox Code Playgroud)

编译器将其替换为零移动.获取相同数量的字节,但需要访问两个内存而不是一个,并且寄存器被烧毁.和三个执行指令而不是一个.所以零点移动明显更好.

现在,如果它是字节大小并在寄存器中:

13: b0 00                   mov    $0x0,%al
15: 30 c0                   xor    %al,%al
Run Code Online (Sandbox Code Playgroud)

代码大小没有区别.(但他们仍然执行不同).


现在,如果你在谈论另一个处理器,那就让我们说吧

   0:   e3a00000    mov r0, #0
   4:   e0200000    eor r0, r0, r0
   8:   e3a00000    mov r0, #0
   c:   e5810000    str r0, [r1]
  10:   e5910000    ldr r0, [r1]
  14:   e0200000    eor r0, r0, r0
  18:   e5810000    str r0, [r1]
Run Code Online (Sandbox Code Playgroud)

你不使用xor(exclusive或,eor)来保存任何东西:一条指令是一条指令,它既可以被取出也可以被执行.如果你在寄存器中有变量的地址,就像任何处理器一样在ram中编写一些东西.如果必须将数据复制到另一个寄存器以执行xor,那么最终仍然会有两次内存访问和三条指令.如果你有一个可以对内存执行内存的处理器,那么零移动会更便宜,因为你只有一个内存访问权限和一个或两个指令,具体取决于处理器.

事实上,它比这更糟糕的: eor r0, r0, r0要求对输入的依赖r0,因为内存的排序规则(限制乱序执行).Xor-zeroing总是产生零,但只能帮助x86汇编中的性能.


所以最重要的是它取决于,如果你在x86系统上的汇编程序中讨论寄存器,从8088到现在,xor通常更快,因为指令更小,读取更快,如果你有一个缓存更少的缓存,留下更多的缓存对于其他代码等,同样需要在指令中编码零的非x86可变指令长度处理器也需要更长的指令,更长的获取时间,如果有高速缓存则消耗更多的高速缓存等等.所以xor是更快(通常取决于它如何编码).如果你有条件标志并且你想要移动/ xor来设置零标志,那么它会变得更糟,你可能必须烧掉正确的指令(在某些处理器上,mov不会改变标志).有些处理器有一个特殊的零寄存器,这不是通用的,当你使用它时,你会得到一个零,你可以编码这个非常常见的用例,而不会烧掉更多的指令空间或烧掉额外的指令周期,将零立即加载到寄存器中.例如,msp430,移动0x1234将花费您两个字的指令,但移动0x0000或0x0001和一些其他常量可以在单个指令字中编码.如果你在讨论ram中的变量,读取 - 修改 - 写入两个不计算指令读取的内存周期,所有处理器将对内存产生双击,如果读取导致高速缓存行填充,则会变得更糟(写入将是非常快,但没有读取,写入只能通过缓存并且执行速度非常快,因为处理器可以在写入并行时保持运行(有时候你会获得性能提升,有时候不会,如果你调整的话为了它).x86和可能较旧的处理器是你看到xoring而不是零移动的习惯的原因.对于那些特定的优化,性能增益仍然存在,系统内存仍然非常慢,并且任何额外的内存周期都是昂贵的,同样丢弃的任何高速缓存都是昂贵的.中间正常的编译器,甚至gcc,将检测xor i,i等于i = 0并且根据具体情况选择更好(在平均系统上)指令序列.

获取迈克尔·阿布拉什(Michael Abrash)的禅宗集.好的,用过的副本可以合理的价格(低于50美元),即使你花80美元的副本,这是值得的.尝试超越特定的8088"循环食客"并理解他试图教授的一般思维过程.然后花费尽可能多的时间来拆卸代码,理想情况下可以用于许多不同的处理器.应用你学到的东西......


Nec*_*lis 5

在旧的CPU上(但是在Pentium Pro之后的那些,根据评论)这曾经是这种情况,然而,现在大多数现代CPU都有特殊的热路径,用于零分配(寄存器和良好对齐的变量),应该产生相同的性能.大多数现代编译器都倾向于使用两者的混合,这取决于周围的代码(较旧的MSVC编译器总是XOR在优化的构建中使用,并且它仍然使用了XOR很多,但MOV reg,0在某些情况下也会使用).

这是一个非常微观的优化,所以,除非你因为寄存器依赖性而导致紧密的循环,否则你可以做到最好的套件.但是应该注意,这种使用XOR在大多数情况下占用较少的空间,这对于嵌入式设备或者当您尝试对齐分支目标时非常有用.

这假设你主要是指x86及其派生词,在那个注释中@Pascal给了我一个想法,就是为此提供技术参考.英特尔优化手册有两个部分,即2.1.3.1 Dependancy Breaking Idioms3.5.1.7 Clearing Registers and Dependancy Breaking Idioms.这两个部分基本上提倡使用XOR基于任何形式的寄存器清除的指令,因为它具有依赖于性的依赖性(这消除了延迟).但是在条件代码需要保留的部分中,MOV优先选择0进入寄存器.

  • 在SandyBridge上,xor-zeroing是特殊的,通过寄存器重命名处理,它甚至不使用执行端口.我从来没有听说过有关适用于'mov reg,0`的类似技巧的任何内容,但如果它们存在则会很酷,你有源于此吗? (5认同)
  • 我不清楚你的意思是"零路线的热门路径".你能提供参考吗?作为旁注,'xor reg,reg`比Pentium Pro上的'mov reg,0`慢,因为处理器认为前者依赖于`reg`.在此之前,这个处理器系列中没有乱序执行,之后,处理器学会识别`xor reg,reg`独立于`reg`的先前值. (2认同)