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"循环食客"并理解他试图教授的一般思维过程.然后花费尽可能多的时间来拆卸代码,理想情况下可以用于许多不同的处理器.应用你学到的东西......
在旧的CPU上(但是在Pentium Pro之后的那些,根据评论)这曾经是这种情况,然而,现在大多数现代CPU都有特殊的热路径,用于零分配(寄存器和良好对齐的变量),应该产生相同的性能.大多数现代编译器都倾向于使用两者的混合,这取决于周围的代码(较旧的MSVC编译器总是XOR在优化的构建中使用,并且它仍然使用了XOR很多,但MOV reg,0在某些情况下也会使用).
这是一个非常微观的优化,所以,除非你因为寄存器依赖性而导致紧密的循环,否则你可以做到最好的套件.但是应该注意,这种使用XOR在大多数情况下占用较少的空间,这对于嵌入式设备或者当您尝试对齐分支目标时非常有用.
这假设你主要是指x86及其派生词,在那个注释中@Pascal给了我一个想法,就是为此提供技术参考.英特尔优化手册有两个部分,即2.1.3.1 Dependancy Breaking Idioms和3.5.1.7 Clearing Registers and Dependancy Breaking Idioms.这两个部分基本上提倡使用XOR基于任何形式的寄存器清除的指令,因为它具有依赖于性的依赖性(这消除了延迟).但是在条件代码需要保留的部分中,MOV优先选择0进入寄存器.
| 归档时间: |
|
| 查看次数: |
4702 次 |
| 最近记录: |