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

8e2ybdfx  于 2022-12-11  发布在  其他
关注(0)|答案(2)|浏览(144)

此问题在此处已有答案

Why does GCC emit a repeated ret?(1个答案)
4天前关闭。
我正在查看here中的一些递归函数:

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);
    }
}

我检查了反汇编,以检查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

Godbolt example中的一个。
多次退货的目的是什么?是bug吗?
编辑
看起来像是gcc 11.x中出现的。在gcc 10.x下编译时,函数的结尾如下所示:

.L1:
        mov     eax, r8d
        ret
.L6:
        xor     r8d, r8d
        mov     eax, r8d
        ret

11.x版本改为在函数的开头将eax置零,然后在函数体中修改它,从而消除了对额外的mov指令的需要。

fcg9iug3

fcg9iug31#

这是通道排序问题的表现。在优化管道中的某个点,以ret结尾的两个基本块不等效,然后某个通道使它们等效,但随后的任何通道都无法将这两个等效块合并为一个。
在编译器资源管理器中,你可以通过检查两遍之间内部表示的快照来查看编译器优化管道的工作方式。对于愚者,在编译器窗格中选择“Add New〉愚者Tree/RTL”。下面是你的示例,在新窗格中预先选择了有问题的转换之前的快照:https://godbolt.org/z/nTazM5zGG
在转储的末尾,可以看到两个基本块:

65: NOTE_INSN_BASIC_BLOCK 8
   77: use ax:SI
   66: simple_return

43: NOTE_INSN_BASIC_BLOCK 9
    5: ax:SI=0
   38: use ax:SI
   74: NOTE_INSN_EPILOGUE_BEG
   75: simple_return

基本上第二个块的不同之处在于它在返回之前将eax置为0。如果你看下一遍(称为“jump 2”),你会看到它将ax:SI=0指令从基本块9和基本块3提升到基本块2,使BB 9等价于BB 8。
如果使用-fno-crossjumping禁用此优化,则差异将保留到最后,从而使生成的程序集不那么令人惊讶。

p4tfgftt

p4tfgftt2#

结论第一:这是愚者精心设计的优化选择。
如果您在本地使用愚者(gcc -O3 -S)而不是在Godbolt上使用愚者,您可以看到两条ret指令之间存在alignment directives

; top part omitted
.L9:
        ret
        .p2align 4,,10
        .p2align 3
.L6:
        ret
        .cfi_endproc

反汇编后,目标文件在填充区域中包含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

第二条ret指令与16字节边界对齐,而第一条指令没有对齐。这使得处理器在将指令用作远程源的跳转目标时能够更快地加载指令。但是,后续的C return语句与第一条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

相关问题