【问题标题】:Difference between n = 0 and n = n - nn = 0 和 n = n - n 之间的差异
【发布时间】:2009-05-15 09:24:19
【问题描述】:

当我阅读 this 问题时,我记得有人曾经(多年前)告诉我,从汇编程序的角度来看,这两个操作非常不同:

n = 0;

n = n - n;

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

编辑:正如一些回复所指出的,我想这对于编译器来说很容易优化成同样的东西。但我觉得有趣的是,如果编译器有一个完全通用的方法,为什么它们会有所不同。

【问题讨论】:

  • 因为它们不一样?
  • 这就是问题所在。告诉我这件事的人说,“在幕后”他们会产生不同的机器代码,而且一个比另一个快。不幸的是,我不记得完整的论点。

标签: c assembly optimization compiler-construction micro-optimization


【解决方案1】:

编写你常用的汇编代码:

xor eax, eax

而不是

mov eax, 0

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

【讨论】:

  • 是的,你可以说 sub eax,eax。唯一的区别是操作设置的标志。
  • 你不能确定周期。原因不是真正的循环,直接。 xor eax,eax 在 x86 架构上产生比 mov eax,0 (6 字节:66B800000000)更短的指令(3 字节:6631C0)。 sub eax,eax 也产生一个 3 字节的指令。虽然对于当前的处理器,sub 和 xor 之间没有太大区别,但 xor 需要更简单的电路并且有可能更快
  • 绝对正确,这完全是关于隐式助记符参数,从而减少了指令大小。
  • 有些架构甚至有一个特殊的寄存器,它的值总是0。(至少MIPS)
【解决方案2】:

编译器 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

【讨论】:

    【解决方案3】:

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

    移动.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 更有效吗?
    • 如果号码已经在寄存器中,通常会出现这种情况
    • 太棒了。这正是我希望得到的答案。
    • @R.A.:是的,n-n 在 M68000 CPU 上的地址寄存器效率更高。 Moveq.l 对于数据寄存器更快,因为 m68k 只有一个 16 位 ALU,但 sub.l 更通用。两者都需要 16 位内存。有趣的是,clr.l(将寄存器设置为 0)比 moveq.l 慢;)
    【解决方案4】:

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

    【讨论】:

    • 如果 n 是非易失性整数类型,很可能是,但如果 n 是易失性的(正如 mouviciel 指出的那样),和/或如果它是浮点类型。对于浮点数,由于 NaN 和 INF,n-n 并不总是等于 0.0。
    【解决方案5】:

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

    【讨论】:

    • 是的,但我想不出现实生活中的情况,即让 n 不稳定,然后执行 n = n - n
    • 当然可以,但我想不出现实生活中一开始会做 n=n-n 的案例。
    • 感谢您的回复,但至少对我来说,使用“volatile”也很“现实”。这只是一个出于教育目的的理论/假设问题。
    【解决方案6】:

    通过将寄存器从自身减去或与自身进行异或来清零寄存器的汇编语言技术是一种有趣的技术,但它并不能真正转化为 C。

    任何优化的 C 编译器都会在有意义的情况下使用这种技术,并且尝试显式地写出它不太可能实现任何目标。

    【讨论】:

      【解决方案7】:

      在 C 中,如果您的编译器很糟糕,它们只会有所不同(对于整数类型)(或者您像 MSVC 答案显示的那样禁用了优化)。

      也许以这种方式告诉你的人试图使用 C 语法描述像 sub reg,reg 这样的 asm 指令,不是谈论这样的语句将如何实际上编译现代优化编译器?在这种情况下,我不会对大多数 x86 CPU 说“非常不同”;大多数 do 特殊情况 sub same,same 作为归零习惯用法,如 xor same,sameWhat is the best way to set a register to zero in x86 assembly: xor, mov or and?

      这使得 asm sub reg,reg 类似于 mov reg,0,但代码大小更好。 (但是,是的,英特尔 P6 系列上的部分寄存器重命名有一些独特的好处,您只能从归零习语中获得,而不是 mov)。


      如果您的编译器尝试在弱排序的 ISA(如 ARM 或PowerPC,其中n=0 打破了对旧值的依赖关系,但n = n-n; 仍然“带有依赖关系”,因此像array[n] 这样的负载将在n = atomic_load_explicit(&shared_var, memory_order_consume) 之后进行依赖排序。更多详情请见Memory order consume usage in C11

      实际上,编译器放弃了尝试正确进行依赖跟踪并将consume 加载提升到acquirehttp://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0371r1.htmlWhen should you not use [[carries_dependency]]?

      但是在弱序 ISA 的 asm 中,需要sub dst, same, same 仍然携带对输入寄存器的依赖,就像在 C 中一样。(大多数弱序 ISA 是具有固定 -宽度指令,因此避免立即操作数不会使机器代码更小。因此,即使在像 ARM 这样没有架构零寄存器的 ISA 上,也没有像 sub r1, r1, r1 这样的较短归零习惯用法的历史用途。mov r1, #0 是与任何其他方式一样大小和至少一样高效。在 MIPS 上,您只需 move $v0, $zero)

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


      只有 x86 特殊情况 sub same,samexor same,same 作为像 mov eax, imm32 这样的破坏依赖的归零习惯用法,因为 mov eax, 0 是 5 个字节,但 xor eax,eax 只有 2 个字节。所以在乱序执行 CPU 之前使用这种窥视孔优化的历史由来已久,而这种 CPU 需要有效地运行现有代码。 What is the best way to set a register to zero in x86 assembly: xor, mov or and? 解释详情。

      除非你用 x86 asm 手写,否则像普通人一样写 0 而不是 n-nn^n,并让编译器使用异或归零作为窥视孔优化。

      其他 ISA 的 Asm 可能有其他窥视孔,例如另一个答案提到了m68k。但同样,如果您使用 C 语言编写,这是编译器的工作。当你的意思是0时,写0。尝试“手持”编译器使用 asm 窥视孔不太可能在禁用优化的情况下工作,并且在启用优化的情况下,如果需要,编译器将有效地将寄存器归零。

      【讨论】:

        【解决方案8】:

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

        n=0
        n=n-n
        

        如果 n 是浮点数,则不总是相等,请参见此处 http://www.codinghorror.com/blog/archives/001266.html

        【讨论】:

        • 如果 n 是无穷大或 NaN - 是的。
        【解决方案9】:

        以下是n = 0n = n - n 行为不同的一些极端情况:

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

        • 如果禁用优化,编译器可能会为这 2 个表达式生成不同的代码,即使是普通的 int n,这可能会也可能不会快速执行。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-03-19
          • 2018-10-05
          • 1970-01-01
          • 2018-08-12
          • 1970-01-01
          • 1970-01-01
          • 2020-09-18
          • 2015-02-23
          相关资源
          最近更新 更多