C语言 为什么不对类 MEMORY 的类型执行尾调用优化



>我试图理解系统 V AMD64 - ABI 的含义,用于从函数中按值返回。

对于以下数据类型

struct Vec3{
double x, y, z;
};

Vec3类型是 MEMORY 类,因此 ABI 指定了以下内容关于"值的返回":

  1. 如果类型具有类 MEMORY,则调用方为返回值提供空间,并在 %rdi 中传递此存储的地址,就像 这是函数的第一个参数。实际上,此地址 成为"隐藏"的第一个参数。此存储不得与任何重叠 通过此参数以外的其他名称对被调用方可见的数据。

    上 返回 %rax 将包含已传入的地址 %rdi 中的调用方。

考虑到这一点,以下(愚蠢的)函数:

struct Vec3 create(void);
struct Vec3 use(){
return create();
}

可以编译为:

use_v2:
jmp     create

在我看来,可以执行尾调用优化,正如我们得到 ABI 的保证,create会将%rdi传递的值放入寄存器%rax

然而,没有一个编译器(gcc,clang,icc)似乎在执行这种优化(在godbolt上)。生成的汇编代码将%rdi保存在堆栈上,只是为了能够将其值移动到%rax,例如 gcc:

use:
pushq   %r12
movq    %rdi, %r12
call    create
movq    %r12, %rax
popq    %r12
ret

无论是对于这个最小的、愚蠢的函数,还是现实生活中更复杂的函数,都会执行尾调用优化。这让我相信,我一定错过了什么,这禁止了它。


不用说,对于类 SSE 的类型(例如只有 2 个而不是 3 个双精度),执行尾调用优化(至少通过 gcc 和 clang,在 godbolt 上生存):

struct Vec2{
double x, y;
};
struct Vec2 create(void);
struct Vec2 use(){
return create();
}

结果在

use:
jmp     create

看起来是一个遗漏的优化错误,您应该报告,如果还没有为 gcc 和 clang 打开重复项。

(在这种情况下,gcc 和 clang 都有相同的错过优化的情况并不少见;不要仅仅因为编译器不这样做就认为某事是非法的。唯一有用的数据是编译器何时执行优化:它要么是编译器错误,要么至少一些编译器开发人员根据他们对任何标准的解释认为它是安全的。


我们可以看到 GCC 正在返回自己的传入参数,而不是返回create()将在 RAX 中返回的副本。这是阻止尾调用优化的遗漏优化。

ABI 需要一个具有 MEMORY 类型返回值的函数来返回 RAX1中的"隐藏"指针。

GCC/clang 已经意识到他们可以通过传递自己的返回值空间来避免实际的复制,而不是分配新空间。 但是要进行尾调用优化,他们必须意识到他们可以将被调用方的 RAX 值保留在 RAX 中,而不是将传入的 RDI 保存在呼叫保留的寄存器中。

如果 ABI 不需要在 RAX 中返回隐藏指针,我希望 gcc/clang 将传入的 RDI 作为优化尾调用的一部分传递不会有问题。

一般来说,编译器喜欢缩短依赖链;这可能就是这里发生的事情。 编译器不知道从rdi参数到raxcreate()结果的延迟可能只是一条mov指令。 具有讽刺意味的是,如果被调用方保存/恢复一些调用保存的寄存器(如r12),引入返回地址指针的存储/重新加载,这可能是一种悲观。 (但这主要只在任何东西使用它时才重要。 我确实得到了一些 clang 代码来这样做,见下文。


脚注 1:返回指针听起来是个好主意,但调用方几乎总是已经知道它将 arg 放在自己的堆栈帧中的位置,并且只会使用像8(%rsp)这样的寻址模式,而不是实际使用 RAX。 至少在编译器生成的代码中,RAX 返回值通常不会被使用。 (如有必要,调用方始终可以自己将其保存在某个地方。

如 如何防止将函数参数用作隐藏指针? 使用调用方堆栈帧中的空间以外的任何内容来接收 retval 存在严重障碍。

如果调用方想要将地址存储在某个地方,如果它是静态地址或堆栈地址,将指针放在寄存器中只是在调用方中保存 LEA。

但是,这种情况接近于有用的情况。如果我们将自己的 retval 空间传递给子函数,我们可能需要在调用后修改该空间。 然后,它对于轻松访问该空间很有用,例如,在我们返回之前修改返回值。

#define T struct Vec3
T use2(){
T tmp = create();
tmp.y = 0.0;
return tmp;
}

高效的手写 asm:

use2:
callq   create
movq    $0, 8(%rax)
retq

与GCC9.1复制相比,实际的clang asm至少仍然使用返回值优化。 (戈博尔特)

# clang -O3
use2:                                   # @use2
pushq   %rbx
movq    %rdi, %rbx
callq   create
movq    $0, 8(%rbx)
movq    %rbx, %rax
popq    %rbx
retq

此 ABI 规则可能专门针对这种情况存在,或者 ABI 设计人员可能正在想象 retval 空间可能是新分配的动态存储(如果 ABI 未在 RAX 中提供指针,则调用方必须保存指向该存储的指针)。 我没有尝试那个案子。

系统 V AMD64 - ABI 将从寄存器中的函数返回数据RDXRAXXMM0XMM1。看看Godbolt,优化似乎是基于大小的。编译器最多只会在寄存器中返回 2double或 4float


编译器总是错过优化。与 Scheme 不同,C 语言没有尾调用优化。GCC和Clang表示,他们没有计划尝试保证尾部调用优化。听起来OP可以尝试询问编译器开发人员或使用所述编译器打开错误。

最新更新