当我读到这个问题时,我记得有人曾经(多年前)告诉我,从汇编程序的角度来看,这两个操作非常不同:
n = 0;
n = n - n;
这是真的吗?如果是,为什么会这样?
编辑:正如一些回复所指出的,我想这对于编译器来说很容易优化成同样的东西。但我发现有趣的是,如果编译器有一个完全通用的方法,为什么它们会有所不同。
当我读到这个问题时,我记得有人曾经(多年前)告诉我,从汇编程序的角度来看,这两个操作非常不同:
n = 0;
n = n - n;
这是真的吗?如果是,为什么会这样?
编辑:正如一些回复所指出的,我想这对于编译器来说很容易优化成同样的东西。但我发现有趣的是,如果编译器有一个完全通用的方法,为什么它们会有所不同。
编写你经常使用的汇编代码:
xor eax, eax
代替
mov eax, 0
那是因为第一条语句只有操作码,没有涉及的参数。您的 CPU 将在 1 个 cylce(而不是 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
优化编译器将为两者生成相同的汇编代码。
在早期,内存和 CPU 周期很少。这导致了很多所谓的“窥视孔优化”。让我们看一下代码:
移动.l #0,d0 moveq.l #0,d0 sub.l a0,a0
第一条指令需要两个字节作为操作码,然后是四个字节作为值 (0)。这意味着浪费了四个字节,而且您需要访问内存两次(一次用于操作码,一次用于数据)。慢。
moveq.l
更好,因为它将数据合并到操作码中,但它只允许将 0 到 7 之间的值写入寄存器。而且您仅限于数据寄存器,没有快速清除地址寄存器的方法。您必须清除数据寄存器,然后将数据寄存器加载到地址寄存器中(两个操作码。不好。)。
这导致对任何寄存器起作用的最后一个操作,只需要两个字节,一个内存读取。翻译成C,你会得到
n = n - n;
这适用于最常用的类型n
(整数或指针)。
这可能取决于是否n
声明为volatile
。
通过从自身减去寄存器或与自身进行异或来清零寄存器的汇编语言技术是一种有趣的技术,但它并不能真正转化为 C。
如果有意义,任何优化的 C 编译器都会使用这种技术,并且尝试显式地写出它不太可能实现任何目标。
在 C 中,如果您的编译器很糟糕(或者您禁用了 MSVC 答案所示的优化),它们只会有所不同(对于整数类型)。
也许以这种方式告诉您的人试图描述一条 asm 指令,如sub reg,reg
使用 C 语法,而不是谈论这样的语句如何用现代优化编译器实际编译?在这种情况下,我不会对大多数 x86 CPU 说“非常不同”;大多数将特殊情况sub same,same
作为归零习语,例如xor same,same
. 在 x86 汇编中将寄存器设置为零的最佳方法是什么:xor、mov 或 and?
这使得 asmsub reg,reg
类似于mov reg,0
,具有更好的代码大小。(但是,是的,英特尔 P6 系列上的部分寄存器重命名有一些独特的好处,您只能从归零习语中获得,而不是mov
)。
如果您的编译器试图在ARM 或 PowerPC 之类的弱排序 ISA 上实现最不推荐使用的语义,它们在 C 中可能会有所不同,其中破坏了对旧值的依赖,但仍然“携带依赖”,所以像这样的负载在 之后进行依赖排序。有关更多详细信息,请参阅C11 中的内存顺序消耗使用情况memory_order_consume
<stdatomic.h>
n=0
n = n-n;
array[n]
n = atomic_load_explicit(&shared_var, memory_order_consume)
在实践中,编译器放弃了尝试正确地进行依赖跟踪并将consume
负载提升到acquire
. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0371r1.html什么时候不应该使用[[carries_dependency]]?
但是在弱序 ISA 的 asm 中,sub dst, same, same
仍然需要对输入寄存器进行依赖,就像在 C 中一样。(大多数弱序 ISA 是具有固定宽度指令的 RISC,因此避免立即操作数不会使机器代码更小。因此,sub r1, r1, r1
即使在没有架构零寄存器的 ARM 等 ISA 上, 也没有使用更短的归零习惯用法。mov r1, #0
大小相同,至少与任何其他方式一样有效。在 MIPS 上,您只需move $v0, $zero
)
所以是的,对于那些非 x86 ISA,它们在 asm 中非常不同。 n=0
避免对变量(寄存器)的旧值的任何错误依赖,同时在旧值准备好n=n-n
之前无法执行。n
只有 x86 的特殊情况 sub same,same
和xor same,same
像mov eax, imm32
, 因为mov eax, 0
是 5 个字节但xor eax,eax
只有 2 个字节这样的依赖关系破坏归零习惯用法。因此,在乱序执行 CPU 之前使用这种窥视孔优化的历史由来已久,这样的 CPU 需要运行现有代码有效。 在 x86 汇编中将寄存器设置为零的最佳方法是什么:xor、mov 或 and?解释细节。
除非您是在 x86 asm 中手动编写,否则请0
像普通人一样编写而不是n-n
or n^n
,并让编译器使用 xor-zeroing 作为窥视孔优化。
其他 ISA 的 Asm 可能有其他窥视孔,例如另一个答案提到 m68k。但同样,如果您使用 C 语言编写,这是编译器的工作。0
当你的意思写0
。尝试“手持”编译器使用 asm 窥视孔不太可能在禁用优化的情况下工作,并且在启用优化的情况下,如果需要,编译器将有效地将寄存器归零。
不确定组装等,但一般来说,
n=0
n=n-n
如果 n 是浮点数,则并不总是相等,请参见此处 http://www.codinghorror.com/blog/archives/001266.html
n = 0
以下是一些极端情况,其中和的行为不同n = n - n
:
如果n
具有浮点类型,则结果将不同于0
特定值:-0.0
, Infinity
, -Infinity
, NaN
...
ifn
定义为volatile
:第一个表达式将生成一个存储到相应的内存位置,而第二个表达式将生成两个加载和一个存储,此外,如果n
是硬件寄存器的位置,这两个加载可能会产生不同的值,导致写入存储非0
值。
如果优化被禁用,编译器可能会为这两个表达式生成不同的代码,即使对于 plain int n
,这可能会或可能不会以速度执行。