为什么不对 class MEMORY 类型执行尾调用优化?
Why is tailcall optimization not performed for types of class MEMORY?
我正在尝试理解 System V AMD64 - ABI 从函数按值返回的含义。
对于以下数据类型
struct Vec3{
double x, y, z;
};
Vec3
类型属于 class MEMORY,因此 ABI 指定了关于 "Returning of Values" 的以下内容:
If the type has class MEMORY, then the caller provides space for the return value and passes the address of this storage in %rdi as if
it were the first argument to the function. In effect, this address
becomes a “hidden” first argument. This storage must not overlap any
data visible to the callee through other names than this argument.
On
return %rax will contain the address that has been passed in by the
caller in %rdi.
考虑到这一点,以下(愚蠢的)功能:
struct Vec3 create(void);
struct Vec3 use(){
return create();
}
可以编译为:
use_v2:
jmp create
在我看来,正如 ABI 向我们保证的那样,可以执行尾调用优化,create
会将 %rdi
传递的值放入 %rax
寄存器。
但是,none 的编译器(gcc、clang、icc)似乎正在执行此优化(此处 on godbolt)。生成的汇编代码将 %rdi
保存在堆栈上,只是为了能够将其值移动到 %rax
,例如 gcc:
use:
pushq %r12
movq %rdi, %r12
call create
movq %r12, %rax
popq %r12
ret
无论是对于这个最小的、愚蠢的函数还是对于现实生活中更复杂的函数,都不会执行尾调用优化。这让我相信,我一定是遗漏了一些禁止它的东西。
不用说,对于 class 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
System V AMD64 - ABI will return data from a function in registers RDX
and RAX
or XMM0
and XMM1
. Looking at Godbolt 优化似乎是基于大小。编译器只会 return 最多 2 double
或 4 float
在寄存器中。
编译器总是错过优化。与 Scheme 不同,C 语言没有尾调用优化。 GCC 和 Clang 表示他们没有计划尝试并保证尾调用优化。听起来 OP 可以尝试询问编译器开发人员或使用所述编译器打开错误。
如果还没有为 gcc 和 clang 打开的副本,您应该报告一个遗漏的优化错误。
(在这种情况下,gcc 和 clang 都有相同的错过优化的情况并不少见;do not 仅仅因为编译器就认为某些事情是非法的不要这样做。 唯一有用的数据是当编译器 执行 执行优化时:它要么是编译器错误,要么至少一些编译器开发人员根据他们对任何标准的解释。)
我们可以看到 GCC 正在 returning 它自己的传入 arg 而不是 returning 它的副本,create()
将在 RAX 中 return。 这 是阻止尾调用优化的错过优化。
ABI 需要一个具有 MEMORY 类型 return 值的函数到 return RAX1 中的 "hidden" 指针。
GCC/clang 确实已经意识到他们可以通过传递自己的 return-值 space 来省略实际复制,而不是分配新的 space。但是要进行尾调用优化,他们必须意识到他们可以将被调用者的 RAX 值保留在 RAX 中,而不是将传入的 RDI 保存在调用保留寄存器中。
如果 ABI 不需要 returning RAX 中的隐藏指针,我预计 gcc/clang 将传入的 RDI 作为优化尾调用的一部分传递没有问题。
通常编译器喜欢缩短依赖链;这可能就是这里发生的事情。编译器不知道从 rdi
arg 到 create()
的 rax
结果的延迟可能只是一条 mov
指令。具有讽刺意味的是,如果被调用者 saves/restores 一些调用保留寄存器(如 r12
),引入 return 地址指针的 store/reload,这可能是一种悲观。 (但这主要只在有任何东西使用它时才重要。我确实得到了一些 clang 代码来这样做,见下文。)
脚注 1:返回指针听起来是个好主意,但几乎总是调用者已经知道将 arg 放在自己的堆栈帧中的位置,并且只会使用像 8(%rsp)
这样的寻址模式而不是实际使用 RAX。至少在编译器生成的代码中,RAX return 值通常不会被使用。 (如有必要,调用者可以随时将其保存在自己的某个地方。)
正如 中所讨论的,在调用者的堆栈帧中使用 space 以外的任何东西来接收 retval 存在严重障碍。
如果调用者想将地址存储在某个地方(如果它是静态地址或堆栈地址),则将指针放在寄存器中只会在调用者中保存一个 LEA。
但是,这种情况接近 有用的情况。 如果我们传递自己的 retval space 到子函数,我们可能希望在调用后 修改 即 space。然后它对于轻松访问 space 很有用,例如在 return.
之前修改 return 值
#define T struct Vec3
T use2(){
T tmp = create();
tmp.y = 0.0;
return tmp;
}
高效的手写asm:
use2:
callq create
movq [=11=], 8(%rax)
retq
实际的 clang asm 至少仍然使用 return-value 优化,而不是 GCC9.1 复制。 (Godbolt)
# clang -O3
use2: # @use2
pushq %rbx
movq %rdi, %rbx
callq create
movq [=12=], 8(%rbx)
movq %rbx, %rax
popq %rbx
retq
这个 ABI 规则可能专门针对这种情况而存在,或者 ABI 设计者可能想象 retval space 可能是新分配的动态存储(调用者 会 如果 ABI 没有在 RAX 中提供它,则必须保存一个指针)。我没试过那种情况。
我正在尝试理解 System V AMD64 - ABI 从函数按值返回的含义。
对于以下数据类型
struct Vec3{
double x, y, z;
};
Vec3
类型属于 class MEMORY,因此 ABI 指定了关于 "Returning of Values" 的以下内容:
If the type has class MEMORY, then the caller provides space for the return value and passes the address of this storage in %rdi as if it were the first argument to the function. In effect, this address becomes a “hidden” first argument. This storage must not overlap any data visible to the callee through other names than this argument.
On return %rax will contain the address that has been passed in by the caller in %rdi.
考虑到这一点,以下(愚蠢的)功能:
struct Vec3 create(void);
struct Vec3 use(){
return create();
}
可以编译为:
use_v2:
jmp create
在我看来,正如 ABI 向我们保证的那样,可以执行尾调用优化,create
会将 %rdi
传递的值放入 %rax
寄存器。
但是,none 的编译器(gcc、clang、icc)似乎正在执行此优化(此处 on godbolt)。生成的汇编代码将 %rdi
保存在堆栈上,只是为了能够将其值移动到 %rax
,例如 gcc:
use:
pushq %r12
movq %rdi, %r12
call create
movq %r12, %rax
popq %r12
ret
无论是对于这个最小的、愚蠢的函数还是对于现实生活中更复杂的函数,都不会执行尾调用优化。这让我相信,我一定是遗漏了一些禁止它的东西。
不用说,对于 class 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
System V AMD64 - ABI will return data from a function in registers RDX
and RAX
or XMM0
and XMM1
. Looking at Godbolt 优化似乎是基于大小。编译器只会 return 最多 2 double
或 4 float
在寄存器中。
编译器总是错过优化。与 Scheme 不同,C 语言没有尾调用优化。 GCC 和 Clang 表示他们没有计划尝试并保证尾调用优化。听起来 OP 可以尝试询问编译器开发人员或使用所述编译器打开错误。
如果还没有为 gcc 和 clang 打开的副本,您应该报告一个遗漏的优化错误。
(在这种情况下,gcc 和 clang 都有相同的错过优化的情况并不少见;do not 仅仅因为编译器就认为某些事情是非法的不要这样做。 唯一有用的数据是当编译器 执行 执行优化时:它要么是编译器错误,要么至少一些编译器开发人员根据他们对任何标准的解释。)
我们可以看到 GCC 正在 returning 它自己的传入 arg 而不是 returning 它的副本,create()
将在 RAX 中 return。 这 是阻止尾调用优化的错过优化。
ABI 需要一个具有 MEMORY 类型 return 值的函数到 return RAX1 中的 "hidden" 指针。
GCC/clang 确实已经意识到他们可以通过传递自己的 return-值 space 来省略实际复制,而不是分配新的 space。但是要进行尾调用优化,他们必须意识到他们可以将被调用者的 RAX 值保留在 RAX 中,而不是将传入的 RDI 保存在调用保留寄存器中。
如果 ABI 不需要 returning RAX 中的隐藏指针,我预计 gcc/clang 将传入的 RDI 作为优化尾调用的一部分传递没有问题。
通常编译器喜欢缩短依赖链;这可能就是这里发生的事情。编译器不知道从 rdi
arg 到 create()
的 rax
结果的延迟可能只是一条 mov
指令。具有讽刺意味的是,如果被调用者 saves/restores 一些调用保留寄存器(如 r12
),引入 return 地址指针的 store/reload,这可能是一种悲观。 (但这主要只在有任何东西使用它时才重要。我确实得到了一些 clang 代码来这样做,见下文。)
脚注 1:返回指针听起来是个好主意,但几乎总是调用者已经知道将 arg 放在自己的堆栈帧中的位置,并且只会使用像 8(%rsp)
这样的寻址模式而不是实际使用 RAX。至少在编译器生成的代码中,RAX return 值通常不会被使用。 (如有必要,调用者可以随时将其保存在自己的某个地方。)
正如
如果调用者想将地址存储在某个地方(如果它是静态地址或堆栈地址),则将指针放在寄存器中只会在调用者中保存一个 LEA。
但是,这种情况接近 有用的情况。 如果我们传递自己的 retval space 到子函数,我们可能希望在调用后 修改 即 space。然后它对于轻松访问 space 很有用,例如在 return.
之前修改 return 值#define T struct Vec3
T use2(){
T tmp = create();
tmp.y = 0.0;
return tmp;
}
高效的手写asm:
use2:
callq create
movq [=11=], 8(%rax)
retq
实际的 clang asm 至少仍然使用 return-value 优化,而不是 GCC9.1 复制。 (Godbolt)
# clang -O3
use2: # @use2
pushq %rbx
movq %rdi, %rbx
callq create
movq [=12=], 8(%rbx)
movq %rbx, %rax
popq %rbx
retq
这个 ABI 规则可能专门针对这种情况而存在,或者 ABI 设计者可能想象 retval space 可能是新分配的动态存储(调用者 会 如果 ABI 没有在 RAX 中提供它,则必须保存一个指针)。我没试过那种情况。