【问题标题】:Why is tailcall optimization not performed for types of class MEMORY?为什么不对类 MEMORY 类型执行尾调用优化?
【发布时间】:2019-12-26 23:37:17
【问题描述】:

我正在尝试理解 System V AMD64 - ABI 从函数按值返回的含义。

对于以下数据类型

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

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

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

    开启 return %rax 将包含由 %rdi 中的调用者。

考虑到这一点,以下(愚蠢的)功能:

struct Vec3 create(void);

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

可以编译为:

use_v2:
        jmp     create

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

但是,似乎没有任何编译器(gcc、clang、icc)执行此优化(此处为on godbolt)。生成的汇编代码将 %rdi 保存在堆栈上,只是为了能够将其值移动到 %rax,例如 gcc:

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

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


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

struct Vec2{
    double x, y;
};

struct Vec2 create(void);

struct Vec2 use(){
    return create();
}

结果

use:
        jmp     create

【问题讨论】:

  • 至少他们这么想,gcc的优化转储显示“ = create(); [return slot optimization] [tail call]”。

标签: c gcc optimization x86-64 calling-convention


【解决方案1】:

如果没有针对 gcc 和 clang 的重复打开,您应该报告一个错过的优化错误。

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


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

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

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

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

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


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

正如What prevents the usage of a function argument as hidden pointer? 中所讨论的,使用调用者堆栈帧中的空间以外的任何内容来接收 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

实际的 clang asm 至少仍然使用返回值优化,而不是 GCC9.1 复制。 (Godbolt)

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

这个 ABI 规则可能专门针对这种情况而存在,或者 ABI 设计者可能认为 retval 空间可能是新分配的动态存储(调用者 必须保存指向 if 的指针ABI 没有在 RAX 中提供)。这种情况我没试过。

【讨论】:

【解决方案2】:

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


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

【讨论】:

  • 这就是为什么返回值具有类“MEMORY”的原因,就像问题所问的那样。 (顺便说一句,在 RDX:RAX 中返回最多 16 个字节的整数结构,而不是 RDI。RAI 不是寄存器名称)。无论如何,x86-64 SysV 将一个额外的指针 arg 传递给函数必须返回和写入的返回值空间。此指针始终在 RDI 中传递并在 RAX 中返回。
  • 好的,但这并不能解决这个答案的根本问题。 OP 不是在问 为什么 它具有 MEMORY 类型,而是在问该案例错过的尾调用优化。
  • (顺便说一句,Vec2 在 XMM1 和 XMM0 的低位 qword 中返回。godbolt.org/z/lfLQv2。仅 FP 结构不会在整数寄存器中传递/返回)
猜你喜欢
  • 2013-05-08
  • 1970-01-01
  • 1970-01-01
  • 2012-01-04
  • 2013-06-12
  • 2010-10-23
  • 2012-04-18
  • 2011-07-16
  • 2021-07-20
相关资源
最近更新 更多