此问题在此处已有答案:
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
指令的需要。
2条答案
按热度按时间fcg9iug31#
这是通道排序问题的表现。在优化管道中的某个点,以
ret
结尾的两个基本块不等效,然后某个通道使它们等效,但随后的任何通道都无法将这两个等效块合并为一个。在编译器资源管理器中,你可以通过检查两遍之间内部表示的快照来查看编译器优化管道的工作方式。对于愚者,在编译器窗格中选择“Add New〉愚者Tree/RTL”。下面是你的示例,在新窗格中预先选择了有问题的转换之前的快照:https://godbolt.org/z/nTazM5zGG
在转储的末尾,可以看到两个基本块:
和
基本上第二个块的不同之处在于它在返回之前将
eax
置为0。如果你看下一遍(称为“jump 2”),你会看到它将ax:SI=0
指令从基本块9和基本块3提升到基本块2,使BB 9等价于BB 8。如果使用
-fno-crossjumping
禁用此优化,则差异将保留到最后,从而使生成的程序集不那么令人惊讶。p4tfgftt2#
结论第一:这是愚者精心设计的优化选择。
如果您在本地使用愚者(
gcc -O3 -S
)而不是在Godbolt上使用愚者,您可以看到两条ret
指令之间存在alignment directives:反汇编后,目标文件在填充区域中包含NOP:
第二条
ret
指令与16字节边界对齐,而第一条指令没有对齐。这使得处理器在将指令用作远程源的跳转目标时能够更快地加载指令。但是,后续的Creturn
语句与第一条ret
指令非常接近,因此它们不会从跳转到对齐的目标中获益。这种对齐在我的Zen 2 CPU上更加明显,
-mtune=native
增加了更多的填充字节: