当我阅读这个问题时,我想起有人曾经告诉过我(很多年前),从汇编器的角度来看,这两个操作非常不同:
n = 0;
n = n - n;
这是真的吗?如果是,为什么会这样?
编辑:正如一些回复所指出的那样,我猜编译器很容易将其优化为相同的结果。但我认为有趣的是,如果编译器采用完全通用的方法,它们为什么会存在差异。
当我阅读这个问题时,我想起有人曾经告诉过我(很多年前),从汇编器的角度来看,这两个操作非常不同:
n = 0;
n = n - n;
这是真的吗?如果是,为什么会这样?
编辑:正如一些回复所指出的那样,我猜编译器很容易将其优化为相同的结果。但我认为有趣的是,如果编译器采用完全通用的方法,它们为什么会存在差异。
当你编写汇编代码时,经常使用:
xor eax, eax
代替
mov eax, 0
这是因为在第一个语句中,你只有操作码而没有参与的参数。你的CPU将在1个周期内执行它(而不是2个周期)。我认为你的情况类似(虽然使用了sub)。
编译器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
move.l #0,d0第一条指令需要两个字节的操作码,然后是四个字节的值(0)。这意味着浪费了四个字节,而且你需要访问内存两次(一次是为了操作码,一次是为了数据)。非常慢。
moveq.l #0,d0
sub.l a0,a0
moveq.l
更好,因为它会将数据合并到操作码中,但它只允许将值写入寄存器中的0到7之间。而且你只能使用数据寄存器,没有快速清除地址寄存器的方法。你必须清除一个数据寄存器,然后将数据寄存器加载到地址寄存器中(两个操作码。很糟糕)。n = n - n;
这将适用于大多数常用的n
类型(整数或指针)。
将寄存器清零的汇编语言技术(通过自减或者异或)是一个非常有趣的方法,但它并不能很好地转换为C语言。
任何优化过的C编译器都会在合适的情况下使用这种技术,因此试图明确地写出它不太可能实现任何优化。
在 C 语言中,它们(整数类型)只有在编译器不好的情况下才有区别(或者您像 MSVC 的答案一样禁用了优化)。
也许告诉你这个的人试图用 C 语法描述汇编指令,比如 sub reg,reg
,而不是讨论这样的语句实际上如何在现代优化编译器中编译?对于大多数 x86 CPU,我不会说“非常不同”;大多数 CPU 都将 sub same,same
作为零值习惯用法的特例处理,就像 xor same,same
一样。 在 x86 汇编中将寄存器设置为零的最佳方法是什么:xor、mov 还是 and?
这使得汇编 sub reg,reg
类似于 mov reg,0
,并且代码大小略微更小。(但是是的,在 Intel P6 家族的部分寄存器重命名方面,某些唯一的好处只能从零值习惯用法中获得,而不是从 mov
中获得。)
如果你的编译器试图在像ARM或PowerPC这样的弱序ISA上实现已大多被弃用的<stdatomic.h>
中的memory_order_consume
语义,则它们可能在C中有所不同。在这种情况下,n=0
会打破对旧值的依赖关系,但n=n-n;
仍然“带有依赖性”,因此像array[n]
这样的加载操作将在n = atomic_load_explicit(&shared_var, memory_order_consume)
之后进行依赖排序。有关更多详细信息,请参见C11中的Memory order consume使用。
consume
加载升级为acquire
。http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0371r1.html和何时不应使用[[carries_dependency]]?
但在弱序ISA的汇编中,sub dst, same, same
仍然需要对输入寄存器产生依赖,就像在C中一样。 (大多数弱序ISA都是具有固定宽度指令的RISC,因此避免立即操作数不会使机器代码变小。因此,即使在像ARM这样没有架构零寄存器的ISA上,也没有使用更短的清零习语,如sub r1,r1,r1
。 mov r1,#0
与任何其他方式的大小相同且至少同样有效。在MIPS上,您只需move $v0,$zero
)
因此,对于那些非x86 ISA,它们在asm中非常不同。 n = 0
可以避免对变量(寄存器)旧值的任何错误依赖,而n = n-n
无法执行,直到n
的旧值准备好。
sub same,same
和xor same,same
作为一种依赖破坏的清零惯用语,就像mov eax, imm32
一样。因为mov eax, 0
需要5个字节,而xor eax,eax
只需要2个字节。所以,在乱序执行CPU出现之前,人们长期使用这种窥孔优化技术,并且这样的CPU需要高效地运行现有的代码。什么是在x86汇编中将寄存器设置为零的最佳方法:xor、mov还是and?解释了细节。0
而不是n-n
或n^n
,并让编译器使用xor清零作为一个窥孔优化。0
。试图“手动引导”编译器使用汇编优化是不太可能成功的,如果禁用了优化,启用优化后,编译器会有效地将寄存器清零。虽然不确定您是否了解汇编语言等内容,但通常情况下,
n=0
n=n-n
如果n是浮点数,那么不一定相等,详见此处http://www.codinghorror.com/blog/archives/001266.html
以下是一些特殊情况,其中n = 0
和n = n - n
的行为不同:
如果n
具有浮点类型,则结果将因特定值而异:-0.0
、Infinity
、-Infinity
、NaN
等。
如果n
被定义为volatile
:第一个表达式将生成一个存储到相应内存位置,而第二个表达式将生成两个加载和一个存储。此外,如果n
是硬件寄存器的位置,则2个加载可能产生不同的值,导致写入存储非0
值。
如果禁用了优化,则即使对于普通的int n
,编译器也可能为这两个表达式生成不同的代码,这可能会或可能不会以相同的速度执行。