n = 0和n = n - n之间的差异

时间:2009-05-15 09:24:20

标签: c optimization compiler-construction assembly

当我读到 this 的问题时,我记得曾经告诉过我(很多年前)的人,从装配者角度来看,这两个操作非常不同:

n = 0;

n = n - n;

这是真的,如果是的话,为什么会这样呢?

编辑:正如一些回复所指出的那样,我认为编译器优化到同一个东西会相当容易。但我觉得有趣的是,如果编译器采用完全通用的方法,它们会有所不同。

10 个答案:

答案 0 :(得分:10)

编写经常使用的汇编代码:

xor eax, eax

而不是

mov eax, 0

这是因为第一个语句只有操作码而没有参数。你的CPU将在1 cylce(而不是2)中完成。我认为你的情况类似(尽管使用sub)。

答案 1 :(得分:7)

编译VC ++ 6.0,没有优化:

4:        n = 0;
0040102F   mov         dword ptr [ebp-4],0
5:
6:        n = n - n;
00401036   mov         eax,dword ptr [ebp-4]
00401039   sub         eax,dword ptr [ebp-4]
0040103C   mov         dword ptr [ebp-4],eax

答案 2 :(得分:4)

优化编译器将为两者生成相同的汇编代码。

答案 3 :(得分:4)

可能取决于n是否声明为volatile

答案 4 :(得分:3)

在早期,内存和CPU周期很少。这导致了许多所谓的“窥视孔优化”。我们来看看代码:

move.l #0, d0

moveq.l #0, d0

sub.l a0,a0

第一条指令需要两个字节用于操作码,然后四个字节用于值(0)。这意味着浪费了四个字节加上你需要访问内存两次(一次用于操作码,一次用于数据)。 Sloooow。

moveq.l更好,因为它会将数据合并到操作码中,但它只允许将0到7之间的值写入寄存器。并且您仅限于数据寄存器,没有快速清除地址寄存器的方法。您必须清除数据寄存器,然后将数据寄存器装入地址寄存器(两个操作码。坏。)。

导致最后一个对任何寄存器起作用的操作,只需要两个字节,一个内存读取。翻译成C,你会得到

n = n - n;

适用于最常用的n类型(整数或指针)。

答案 5 :(得分:2)

通过从寄存器中减去寄存器或将寄存器与自身进行异或来汇总寄存器的汇编语言技术很有意思,但它并没有真正转换为C.

任何优化的C编译器都会使用这种技术,如果它有意义,并且尝试明确地写出它不可能实现任何目标。

答案 6 :(得分:2)

在C语言中,仅当编译器无法正常运行时(对于整数类型),它们才会有所不同(或您禁用了优化,如显示MSVC答案所示)。

也许这样告诉您的人试图使用C语法描述类似sub reg,reg的asm指令,谈论这样的语句实际上 用现代的优化编译器编译?在这种情况下,对于大多数x86 CPU,我不会说“非常不同”。大多数{em> do 特殊情况下的sub same,same都是归零成语,例如xor same,same What is the best way to set a register to zero in x86 assembly: xor, mov or and?

这使汇编sub reg,regmov reg,0相似,但代码大小更好。 (但是,是的,在Intel P6系列上进行部分注册重命名具有一些独特的好处,即只能从清零习惯而不是mov中获得)。


如果您的编译器试图在弱序中实现memory_order_acquire中最不推荐使用的 <stdatomic.h>语义,它们在C中可能会有所不同。像ARM或PowerPC这样的ISA,其中n=0打破了对旧值的依赖关系,但是n = n-n;仍然“带有依赖关系”,因此array[n]之类的负载将在{{1之后}}。有关更多详细信息,请参见Memory order consume usage in C11

实际上,编译器放弃了尝试正确进行依赖项跟踪并将n = atomic_load_explicit(&shared_var, memory_order_consume)的负载提升到consume的尝试。 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0371r1.htmlWhen should you not use [[carries_dependency]]?

但是在asm中,对于弱序ISA来说,acquire必需的,以便对输入寄存器进行依赖,就像在C语言中一样。(大多数弱序ISA是具有以下条件的RISC:固定宽度的指令,因此避免立即操作数不会使机器代码变小,因此,即使在像ARM这样的ISA上都没有体系结构零寄存器的情况下,sub dst, same, same之类的较短的归零惯用法也没有历史性的使用。 sub r1, r1, r1的大小相同,且效率至少与其他任何方式相同。在MIPS上,您只需mov r1, #0

是的,对于那些非x86 ISA,它们在asm中有很大的不同move $v0, $zero避免了对变量(寄存器)的旧值的任何错误依赖,而n=0n=n-n的旧值准备好之前无法执行。


仅将x86特殊情况下的nsub same,same用作像xor same,same 这样的打破依赖的清零习惯,因为mov eax, imm32是5个字节,但是mov eax, 0只有2。因此,在无序执行CPU之前使用此窥孔优化已有很长的历史,而这样的CPU需要有效地运行现有代码。 What is the best way to set a register to zero in x86 assembly: xor, mov or and?解释了细节。

除非您用x86 asm手工编写,否则像普通人一样写xor eax,eax而不是0n-n,然后让编译器使用“异或归零”作为窥视孔优化。

其他ISA的asm可能还有其他窥孔,例如另一个答案提到了m68k。但是同样,如果您使用C编写,这就是编译器的工作。表示n^n时写0。在禁用优化的情况下,尝试“手握”编译器使其使用asm窥孔是不太可能的,并且启用优化后,如果需要,编译器将有效地将寄存器清零。

答案 7 :(得分:1)

不确定装配等,但一般来说,

n=0
n=n-n
如果n是浮点,则

总是相等,请参见此处 http://www.codinghorror.com/blog/archives/001266.html

答案 8 :(得分:0)

使用程序集:

and eax, 0 ; n = 0

答案 9 :(得分:0)

在某些极端情况下,n = 0n = n - n的行为不同:

  • 如果n具有浮点类型,则对于以下特定值,结果将与0不同:-0.0Infinity-InfinityNaN ...

  • 如果n被定义为volatile:第一个表达式将在相应的内存位置生成一个存储,而第二个表达式将生成两个加载和一个存储,如果{ {1}}是硬件寄存器的位置,两次加载可能会产生不同的值,从而导致写操作存储非n值。

  • 如果禁用了优化,则编译器可能会为这两个表达式生成不同的代码,即使是对于普通0而言,也可能不会以最快的速度执行。