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

ead*_*ead 12 c optimization gcc x86-64 calling-convention

我试图了解System V AMD64-ABI对于从函数按值返回的含义。

对于以下数据类型

struct Vec3{
    double x, y, z;
};
Run Code Online (Sandbox Code Playgroud)

该类型Vec3属于MEMORY类,因此ABI就“返回值”指定了以下内容:

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

    返回时,%rax将包含调用者在%rdi中传递的地址。

考虑到这一点,以下(傻)功能:

struct Vec3 create(void);

struct Vec3 use(){
    return create();
}
Run Code Online (Sandbox Code Playgroud)

可以编译为:

use_v2:
        jmp     create
Run Code Online (Sandbox Code Playgroud)

我认为,正如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
Run Code Online (Sandbox Code Playgroud)

无论是对于这种最小的,愚蠢的功能,还是对于现实生活中更复杂的功能,都不​​会执行尾调用优化。这使我相信,我必须丢失某些东西,这是禁止的。


毋庸置疑,对于SSE类的类型(例如,仅2而不是3的两倍),将执行尾调用优化(至少由gcc和clang进行,仅靠戈德螺栓

use_v2:
        jmp     create
Run Code Online (Sandbox Code Playgroud)

结果是

use:
        jmp     create
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 7

如果还没有gcc和clang的重复打开,则您似乎应该报告错过的优化错误。

(这并不罕见GCC和铛具有相同错过优化在这样的情况下,不能假定的东西是违法的,只是因为编译器不这样做。 唯一有用的数据是,当编译器执行优化:它要么编译器错误或至少一些编译器开发人员认为,根据他们对任何标准的解释,它是安全的。)


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

ABI需要具有MEMORY类型返回值的函数,以返回RAX 1中的“隐藏”指针。

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

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

通常,编译器喜欢缩短依赖链。这可能就是这里发生的事情。编译器不知道从rdiarg到rax结果的延迟create()可能只是一条mov指令。具有讽刺意味的是,如果被调用者保存/恢复了一些保留呼叫的寄存器(例如r12),并引入了返回地址指针的存储/重新加载,这可能是一种悲观。(但是,这只在有什至使用它的情况下才重要。我确实得到了一些c代码,请参见下文。)


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

正如“ 如何防止将函数参数用作隐藏指针”中讨论的那样?在调用者的堆栈框架中使用除空格以外的其他任何东西来接收撤回消息都存在严重的障碍。

如果调用者希望将地址存储在某个位置(无论是静态地址还是堆栈地址),则将指针保存在寄存器中只会在调用者中保存LEA。

然而,这种情况下是接近一个地方是有益的。 如果我们将自己的检索空间传递给子函数,则可能需要在调用之后修改该空间。这对于轻松访问该空间很有用,例如在返回之前修改返回值。

#define T struct Vec3

T use2(){
    T tmp = create();
    tmp.y = 0.0;
    return tmp;
}
Run Code Online (Sandbox Code Playgroud)

高效的手写Asm:

use2:
        callq   create
        movq    $0, 8(%rax)
        retq
Run Code Online (Sandbox Code Playgroud)

与GCC9.1复制相比,实际的clang asm至少仍使用返回值优化。(Godbolt

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

此ABI规则也许对于这种情况下尤其存在,或者也许是ABI设计师们想象的空间RETVAL可能是新分配的动态存储(其中主叫方有一个指针保存到如果ABI并未RAX提供IT) 。我没有尝试过这种情况。