为什么编译器会复制一些指令?

Ign*_*tov 58 c++ compiler-construction clang

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

int gcd(unsigned x, unsigned y) {
  return x == 0 ? y : gcd(y % x, x);
}
Run Code Online (Sandbox Code Playgroud)

这是汇编代码(由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
Run Code Online (Sandbox Code Playgroud)

在以下代码段中:

  mov eax, ecx
  jne .LBB0_2
  mov eax, ecx
Run Code Online (Sandbox Code Playgroud)

如果跳转没有发生,eax则重新分配没有明显的原因.

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

编译器是不是足够聪明,还是没有理由不删除重复?

jan*_*anm 40

编译器可以执行对人们来说不明显的优化,并且删除指令并不总能使事情变得更快.

少量搜索表明,当RET紧跟在条件分支之后,各种AMD处理器都存在分支预测问题.通过用基本上无操作的方式填充该插槽,可以避免性能问题.

更新:

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

具体来说,避免以下两种情况:

  • 任何类型的分支(有条件或无条件),其具有单字节近返返RET指令作为其目标.请参阅"示例".

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

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

  • 您是否有证据证明编译器确实是出于这个原因而故意这样做的?如果他们想要在RET之前获得NOP,那么这个MOV远非他们本来可以做到的最简单.如果这只影响AMD cpus,-mtune = intel或类似应删除它,但事实并非如此. (9认同)
  • 我有证据表明代码更长并且仍然在 -Os --> 错误处生成。 (2认同)
  • 没有*真正的*NO-OP 指令吗?为什么要使用“本质上”只是 NO-OP 的指令? (2认同)

J_H*_*J_H 5

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

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

听起来像是符号执行技术的机会.如果约束求解器没有找到给定MOV的分支点,那么它实际上可能是NOP.