C语言 为什么gcC语言 o3会生成多个ret指令?



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

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例子。

多重返回的目的是什么?是bug吗?


编辑似乎出现在gcc 11.x。在gcc 10下编译时。X,则函数结束如下:

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

in: store result ineax。11。x版本在函数开头将eax归零,然后在函数体中对其进行修改,从而消除了额外的mov指令的需要。

这是传递排序问题的一种表现。在优化管道的某个点上,以ret结尾的两个基本块是不相等的,然后一些传递使它们相等,但没有后续的传递能够将两个等效块折叠成一个。

在编译器资源管理器中,你可以通过检查内部表示的快照来查看编译器优化管道是如何工作的。对于GCC,选择"添加新";GCC树/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设置为零。如果你看下一遍(称为"jump2"),你会看到它将ax:SI=0指令从基本块9和基本块3提升到基本块2,使bb9等同于bb8。

如果使用-fno-crossjumping禁用此优化,则差异将被传递到最后,使生成的程序集不那么令人惊讶。

首先得出结论:这是GCC有意为之的优化选择。

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

; 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字节边界,而第一个指令不是。这允许处理器在将指令用作来自远程源的跳转目标时更快地加载该指令。然而,随后的return语句与第一个ret指令足够接近,因此它们不会从跳转到对齐的目标中获益。

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

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

最新更新