为什么 GCC 会发出重复的 `ret`?

Ber*_*ach 6 c++ x86 gcc clang

在下面的 C++ 示例中,我定义了一个计算树最左边路径高度的函数。

struct TreeNode {
  int value{};
  TreeNode *l = nullptr;
  TreeNode *r = nullptr;
};

int getLeftHeight(TreeNode *root) {
  int height = 0;
  while (root != nullptr) {
    root = root->l;
    height++;
  }
  return height;
}
Run Code Online (Sandbox Code Playgroud)

使用 GCC 9.3 或 10.1(使用 -O3)编译时,我得到以下 x86。

getLeftHeight(TreeNode*):
        xor     eax, eax
        test    rdi, rdi
        je      .L4
.L3:
        mov     rdi, QWORD PTR [rdi+8]
        add     eax, 1
        test    rdi, rdi
        jne     .L3
        ret
.L4:
        ret
Run Code Online (Sandbox Code Playgroud)

但是,在使用 Clang 10 编译时,如下图所示,没有重复的ret.

getLeftHeight(TreeNode*):
        xor     eax, eax
        test    rdi, rdi
        je      .LBB0_3
.LBB0_1:
        mov     rdi, qword ptr [rdi + 8]
        add     eax, 1
        test    rdi, rdi
        jne     .LBB0_1
.LBB0_3:
        ret
Run Code Online (Sandbox Code Playgroud)

海湾合作委员会为什么要这样做?

据我了解,这ret将需要一个额外的字节进行编码。也许在这种情况下这并不重要,因为发出的机器代码的对齐/填充,但如果不是这种情况,并且这种重复发生在大型代码库中,许多字节的机器代码最终可能只是重复ret的 .

这种特殊的重复情况似乎也很容易发现和优化。

rus*_*tyx 6

这似乎是 GCC 中遗漏的优化。

有一个错误#71923,其中有一个非常相似的示例,标记为“missed-optimization”。

话虽如此,对性能的影响是微不足道的——主要是代码大小。此外,当第二个ret对齐时(现代 x86 目标通常是这种情况),消除第一个ret通常不会对代码大小产生影响,但如果消除,从循环中失败将导致解码nop之前的附加指令ret,这会使该代码路径变慢。

无论如何,始终欢迎向 GCC 提出拉取请求。您可以查看优化过程列表,-fdump-passes并使用-fdump-tree-...标志转储 RTL/GIMPLE 树(或在Godbolt查看)。主要问题是现有的优化通道是否应该解决这个问题,或者是否需要添加一个新的优化通道(我不是 GCC 方面的专家,所以不能以一种或另一种方式提出建议)。