为什么编译器复制一些说明

Why do compilers duplicate some instructions?

本文关键字:说明 复制 编译器 为什么      更新时间:2023-10-16

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

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:一个也可以完美地工作。

编译器根本不够聪明,或者有理由不删除重复?

编译器可以执行对人不明显的优化,并且删除说明并不总是使事情更快。

少量搜索表明,当RET立即在有条件分支之后,各种AMD处理器都有分支预测问题。通过用本质上是一个no-op填充该插槽,可以避免性能问题。

更新:

示例参考," AMD64处理器的软件优化指南"第6.2节(请参阅http://support.amd.com/techdocs/25112.pdf)说:

特别是避免以下两种情况:

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

  • 直接在单字节近返回ret指令之前发生的条件分支。

它还详细介绍了为什么跳跃目标应具有对齐方式,这也可能在功能结束时解释重复的RET。

任何编译器都会有一堆转换,用于重命名,展开,提升等等。结合其输出可能会导致次优案例,例如您所显示的内容。马克·格里斯(Marc Glisse)提供了很好的建议:这是一个错误报告。您正在描述窥视孔优化器的机会,以丢弃

的说明
  • 不影响注册表的状态&完全记忆,或
  • 不影响对函数后条件重要的状态,对其公共API无关紧要。

听起来像是符号执行技术的机会。如果约束求解器找不到给定MOV的分支点,也许它确实是NOP。