【问题标题】:Why do compilers duplicate some instructions?为什么编译器会重复一些指令?
【发布时间】:2025-12-05 20:40:01
【问题描述】:

有时编译器会生成带有奇怪指令重复的代码,这些重复指令可以安全地删除。考虑以下代码:

int gcd(unsigned x, unsigned y) {
  return x == 0 ? y : gcd(y % x, x);
}

这是汇编代码(生成的by clang 5.0 启用了优化):

gcd(unsigned int, unsigned int): # @gcd(unsigned int, unsigned int)
  mov eax, esi
  mov edx, edi
  test edx, edx
  je .LBB0_1
.LBB0_2: # =>This Inner Loop Header: Depth=1
  mov ecx, edx
  xor edx, edx
  div ecx
  test edx, edx
  mov eax, ecx
  jne .LBB0_2
  mov eax, ecx
  ret
.LBB0_1:
  ret

在下面的sn-p中:

  mov eax, ecx
  jne .LBB0_2
  mov eax, ecx

如果没有发生跳转,eax 会无明显原因地重新分配。

另一个例子是函数末尾的两个 ret:一个也可以完美地工作。

是编译器不够智能还是有理由不删除重复项?

【问题讨论】:

  • clangcc++?
  • 有趣的是 gcc 不这样做:godbolt.org/g/MxTiaY.
  • @lisyarus gcc
  • 添加mov 指令对分支预测器有何影响?
  • 请随时向 gcc 和 llvm 报告这个错过的优化。 2 'mov' 位于不同的基本块中,这使得优化有点困难,但仍然是可取的。

标签: c++ compiler-construction clang


【解决方案1】:

任何编译器都会有一堆用于寄存器重命名、展开、提升等的转换。结合它们的输出可能会导致次优情况,例如您所展示的情况。 Marc Glisse 提供了很好的建议:值得报告错误。您正在描述窥视孔优化器丢弃指令的机会

  • 完全不影响寄存器和内存的状态,或者
  • 不影响对函数的后置条件很重要的状态,对其公共 API 无关紧要。

听起来像是symbolic execution 技术的机会。如果约束求解器没有找到给定 MOV 的分支点,那么它可能真的是一个 NOP。

【讨论】:

    【解决方案2】:

    编译器可以执行人们不明显的优化,并且删除指令并不总是让事情变得更快。

    少量搜索表明,当 RET 紧跟在条件分支之后时,各种 AMD 处理器都会出现分支预测问题。通过用本质上是无操作的内容填充该插槽,可以避免性能问题。

    更新:

    示例参考,“AMD64 处理器的软件优化指南”(参见 http://support.amd.com/TechDocs/25112.PDF)第 6.2 节说:

    具体要避免以下两种情况:

    • 以单字节近返回 RET 指令为目标的任何类型的分支(条件或无条件)。请参阅“示例”。

    • 在代码中直接出现在单字节近返回 RET 指令之前的条件分支。

    它还详细说明了为什么跳转目标应该对齐,这也可能解释了函数末尾的重复 RET。

    【讨论】:

    • @Cornstalks this 我是其中之一。
    • 您是否有证据表明编译器确实出于这个原因故意这样做?如果他们想要在 RET 之前获得 NOP,那么这个 MOV 远非他们所能追求的最简单的。如果这只影响 AMD cpus,-mtune=intel 或类似的应该删除它,但它不会。
    • 我有证据表明代码更长并且仍然在 -Os --> 错误处生成。
    • 没有real NO-OP指令吗?为什么要使用“本质上”是 NO-OP 的指令?
    • @TheCodeArtist 是的,我同意——我考虑过这一点,然后发现 AMD 更难的要求。真正的一点是,现代 CPU 上的调度不仅仅是短代码。需要各种管道维护。