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

59

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

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

这是汇编代码(使用启用了优化的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
在下面的代码片段中:
  mov eax, ecx
  jne .LBB0_2
  mov eax, ecx

如果跳跃不发生,eax会被重新分配,但没有明显的原因。

另一个例子是函数末尾有两个ret:其中一个同样可以完美地工作。

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


2
clang, c or c++? - Khalil Khalaf
2
有趣的是gcc不会这样做:https://godbolt.org/g/MxTiaY。 - lisyarus
2
@lisyarus,gcc确实可以做到。 - Justin
6
加入一条“mov”指令会如何影响分支预测器? - Oliver Charlesworth
2
请随意向gcc和llvm报告此未优化情况。这2个“mov”位于不同的基本块中,这使得优化有点困难,但仍然值得。 - Marc Glisse
显示剩余12条评论
2个回答

42

编译器可以执行人类看不出的优化,而删除指令并不总是能使事情更快。

少量搜索显示,当 RET 紧接条件分支时,各种 AMD 处理器存在分支预测问题。通过用本质上是无操作的内容填充该位置,避免了性能问题。

更新:

示例参考,《AMD64 处理器软件优化指南》第 6.2 节 (请参见 http://support.amd.com/TechDocs/25112.PDF) 中提到:

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

  • 任何类型的分支(无论是条件分支还是无条件分支),其目标都是单字节近返回 RET 指令。参见“示例”。

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

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


@Cornstalks 这可能是一个。 - Mário Feroldi
9
你是否有证据表明编译器确实出于这个原因而故意这样做?如果他们想在 RET 之前加入 NOP,那么这个 MOV 肯定不是他们最简单的选择。如果这只影响 AMD 的 CPU,使用 -mtune=intel 或类似选项应该可以解决,但实际上并没有。 - Marc Glisse
2
我有证据表明代码更长,但在-Os下仍然生成-->错误。 - Marc Glisse
2
有没有真正的NO-OP指令?为什么要使用一个“本质上”是NO-OP的指令? - Barmar
1
@TheCodeArtist 是的,我同意--我考虑过这个问题,但发现AMD更难满足要求。真正的问题是,在现代CPU上进行调度不仅仅是关于短代码的问题。需要进行各种流水线维护。 - janm
显示剩余4条评论

5
任何编译器都会有一堆寄存器重命名、展开、提升等转换。将它们的输出组合起来可能会导致像您展示的那样的次优情况。Marc Glisse提供了很好的建议:值得报告一个错误。您正在描述一个机会,即一个窥视优化器可以丢弃不影响寄存器和内存状态或者不影响函数后置条件但对于其公共API无关紧要的指令。
听起来像是符号执行技术的机会。如果约束求解器找不到给定MOV的分支点,那么它可能真的是NOP。

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