n = 0和n = n - n之间的区别是什么?(这是一个关于IT技术的问题)

3

当我阅读这个问题时,我想起有人曾经告诉过我(很多年前),从汇编器的角度来看,这两个操作非常不同:

n = 0;

n = n - n;

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

编辑:正如一些回复所指出的那样,我猜编译器很容易将其优化为相同的结果。但我认为有趣的是,如果编译器采用完全通用的方法,它们为什么会存在差异。


因为它们不相同? - Johan Kotlinski
这就是问题所在。告诉我的人说,“在引擎盖下”,它们将生成不同的机器代码,其中一个比另一个更快。不幸的是,我不记得完整的论据了。 - sharkin
9个回答

11

当你编写汇编代码时,经常使用:

xor eax, eax

代替

mov eax, 0

这是因为在第一个语句中,你只有操作码而没有参与的参数。你的CPU将在1个周期内执行它(而不是2个周期)。我认为你的情况类似(虽然使用了sub)。


1
是的,你可以说 sub eax,eax。唯一的区别是操作设置的标志位。 - anon
你无法确定「循环」。原因并非直接与循环有关。在 x86 架构中,xor eax, eax 可生成较短的指令(3 字节:6631C0),而 mov eax, 0 则需 6 字节(66B800000000)。 sub eax,eax 也可以生成 3 字节指令。尽管当前处理器上 sub 和 xor 没有太大区别,但 xor 需要更简单的电路,并且具有更快速的潜力。 - Mehrdad Afshari
完全正确,这就是隐式助记符参数和因此减小指令大小的全部内容。 - none
有些架构甚至有一个特殊的寄存器,其值始终为0。(至少MIPS如此) - Will

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

6
在早期,内存和CPU周期都很稀缺。这导致了很多所谓的“窥视孔优化”。让我们看看代码:
    move.l #0,d0
moveq.l #0,d0
sub.l a0,a0
第一条指令需要两个字节的操作码,然后是四个字节的值(0)。这意味着浪费了四个字节,而且你需要访问内存两次(一次是为了操作码,一次是为了数据)。非常慢。 moveq.l 更好,因为它会将数据合并到操作码中,但它只允许将值写入寄存器中的0到7之间。而且你只能使用数据寄存器,没有快速清除地址寄存器的方法。你必须清除一个数据寄存器,然后将数据寄存器加载到地址寄存器中(两个操作码。很糟糕)。
这导致了最后一种操作,它适用于任何寄存器,只需要两个字节,单个内存读取。翻译成C语言,你会得到:
n = n - n;

这将适用于大多数常用的n类型(整数或指针)。


你是说 n = n-n 这种变量实际上比 n = 0 更有效吗? - sharkin
如果数字已经在寄存器中,那通常就是这种情况。 - stephan
太棒了。这正是我希望得到的答案。 - sharkin
2
@R.A.:是的,在M68000 CPU上,n-n对于地址寄存器更有效率。因为m68k只有16位ALU,所以moveq.l对于数据寄存器来说更快,但sub.l更通用。两者都需要16位存储器。有趣的是,clr.l(将寄存器设置为0)比moveq.l慢;) - Aaron Digulla

6
一个优化编译器将会为这两个产生相同的汇编代码。

如果n是一个非易失性整数类型,很可能是的,但如果n是易失性的(正如mouviciel指出的那样),或者它是浮点类型,则不一定。对于浮点数,由于NaN和INF,n-n并不总是等于0.0。 - Max Barraclough

5
可能取决于变量n是否声明为volatile。

是的,但我想不出一个现实生活中的案例,其中一个人会使n成为易失性,然后执行n = n - n。 - Eli Bendersky
1
当然可以,但我想不出一个现实生活中需要使用 n=n-n 的情况。 - mouviciel
谢谢回复,但对我来说使用“volatile”也非常“现实”。这只是一个理论/假设性问题,用于教育目的。 - sharkin

4

将寄存器清零的汇编语言技术(通过自减或者异或)是一个非常有趣的方法,但它并不能很好地转换为C语言。

任何优化过的C编译器都会在合适的情况下使用这种技术,因此试图明确地写出它不太可能实现任何优化。


3

在 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加载升级为acquirehttp://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,r1mov r1,#0 与任何其他方式的大小相同且至少同样有效。在MIPS上,您只需move $v0,$zero

因此,对于那些非x86 ISA,它们在asm中非常不同。 n = 0 可以避免对变量(寄存器)旧值的任何错误依赖,而n = n-n 无法执行,直到n的旧值准备好。


只有x86特殊情况下,将sub same,samexor same,same作为一种依赖破坏的清零惯用语,就像mov eax, imm32一样。因为mov eax, 0需要5个字节,而xor eax,eax只需要2个字节。所以,在乱序执行CPU出现之前,人们长期使用这种窥孔优化技术,并且这样的CPU需要高效地运行现有的代码。什么是在x86汇编中将寄存器设置为零的最佳方法:xor、mov还是and?解释了细节。
除非您手动编写x86汇编代码,否则请像普通人一样使用0而不是n-nn^n,并让编译器使用xor清零作为一个窥孔优化。
其他ISA的汇编语言可能有其他的洞口,例如另一个答案提到了m68k。但是,如果你正在使用C语言编写代码,这就是编译器的工作。当你想表示0时,请写0。试图“手动引导”编译器使用汇编优化是不太可能成功的,如果禁用了优化,启用优化后,编译器会有效地将寄存器清零。

2

如果n是无穷大或NaN,则是的。 - Jonathan Leffler

0

以下是一些特殊情况,其中n = 0n = n - n的行为不同:

  • 如果n具有浮点类型,则结果将因特定值而异:-0.0Infinity-InfinityNaN等。

  • 如果n被定义为volatile:第一个表达式将生成一个存储到相应内存位置,而第二个表达式将生成两个加载和一个存储。此外,如果n是硬件寄存器的位置,则2个加载可能产生不同的值,导致写入存储非0值。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接