为什么 gcc -O3 会生成多个 ret 指令?

Lun*_*din 6 c assembly gcc x86-64 micro-optimization

我正在从这里查看一些递归函数:

int get_steps_to_zero(int n)
{
    if (n == 0) {
        // Base case: we have reached zero
        return 0;
    } else if (n % 2 == 0) {
        // Recursive case 1: we can divide by 2
        return 1 + get_steps_to_zero(n / 2);
    } else {
        // Recursive case 2: we can subtract by 1
        return 1 + get_steps_to_zero(n - 1);
    }
}
Run Code Online (Sandbox Code Playgroud)

我检查了反汇编,以检查 gcc 是否管理尾部调用优化/展开。看起来确实如此,尽管使用 x86-64 gcc 12.2 -O3 我得到了一个像这样的函数,以两条ret指令结尾:

get_steps_to_zero:
        xor     eax, eax
        test    edi, edi
        jne     .L5
        jmp     .L6
.L10:
        mov     edx, edi
        shr     edx, 31
        add     edi, edx
        sar     edi
        test    edi, edi
        je      .L9
.L5:
        add     eax, 1
        test    dil, 1
        je      .L10
        sub     edi, 1
        test    edi, edi
        jne     .L5
.L9:
        ret
.L6:
        ret
Run Code Online (Sandbox Code Playgroud)

上帝螺栓的例子

多重回报的目的是什么?这是一个错误吗?


编辑

看起来这是从 gcc 11.x 开始出现的。当在 gcc 10.x 下编译时,函数结束如下:

.L1:
        mov     eax, r8d
        ret
.L6:
        xor     r8d, r8d
        mov     eax, r8d
        ret
Run Code Online (Sandbox Code Playgroud)

如:将结果存储在eax. 11.x 版本eax在函数开头将其归零,然后在函数体中对其进行修改,从而无需额外的mov指令。

iBu*_*Bug -2

先结论:这是GCC刻意的优化选择。

如果您在本地使用 GCC ( ) 而不是在 Godbolt 上,您可以看到两条指令之间gcc -O3 -S存在对齐指令ret

; top part omitted
.L9:
        ret
        .p2align 4,,10
        .p2align 3
.L6:
        ret
        .cfi_endproc
Run Code Online (Sandbox Code Playgroud)

反汇编后的目标文件在该填充区域中包含一个 NOP:

   8:   75 13                   jne    1d <get_steps_to_zero+0x1d>
   a:   eb 24                   jmp    30 <get_steps_to_zero+0x30>
   c:   0f 1f 40 00             nopl   0x0(%rax)
<...>
  2b:   75 f0                   jne    1d <get_steps_to_zero+0x1d>
  2d:   c3                      ret
  2e:   66 90                   xchg   %ax,%ax
  30:   c3                      ret
Run Code Online (Sandbox Code Playgroud)

第二ret条指令与 16 字节边界对齐,而第一条指令则不然。这使得处理器在用作来自远程源的跳转目标时能够更快地加载指令。然而,后续的 Creturn语句与第一条指令足够接近ret,因此它们不会从跳转到对齐的目标中受益。

这种对齐方式在我的 Zen 2 CPU 上更加明显-mtune=native,添加了更多填充字节:

  29:   75 f2                   jne    1d <get_steps_to_zero+0x1d>
  2b:   c3                      ret
  2c:   0f 1f 40 00             nopl   0x0(%rax)
  30:   c3                      ret
Run Code Online (Sandbox Code Playgroud)

  • `ret` 长度为 1 个字节,无条件跳转。在其后面加载后面的指令字节是没有用的,并且包含它的任何块的任何代码获取都将获取整个指令。让它对齐没有任何好处。另外,如果这一推理成立,我们预计早期的 `jmp .L6` 也会以“对齐的” `ret` 为目标,而未对齐的“ret”只能通过失败到达。 (2认同)