【问题标题】:GCC/Clang x86_64 C++ ABI mismatch when returning a tuple?返回元组时 GCC/Clang x86_64 C++ ABI 不匹配?
【发布时间】:2023-03-15 16:24:02
【问题描述】:

在尝试to optimize return values on x86_64 时,我注意到了一件奇怪的事情。即,给定代码:

#include <cstdint>
#include <tuple>
#include <utility>

using namespace std;

constexpr uint64_t a = 1u;
constexpr uint64_t b = 2u;

pair<uint64_t, uint64_t> f() { return {a, b}; }
tuple<uint64_t, uint64_t> g() { return tuple<uint64_t, uint64_t>{a, b}; }

Clang 3.8 outputsf 的这个汇编代码:

movl $1, %eax
movl $2, %edx
retq

这是g

movl $2, %eax
movl $1, %edx
retq

看起来最佳。但是,当compiled with GCC 6.1 时,虽然为f 生成的程序集与Clang 输出的相同,但为g 生成的程序集是:

movq %rdi, %rax
movq $2, (%rdi)
movq $1, 8(%rdi)
ret

看起来返回值的类型被 GCC 归类为 MEMORY,但被 Clang 归类为 INTEGER。我可以确认将 Clang 代码与 GCC 代码链接这样的代码会导致分段错误(Clang 调用 GCC 编译的 g() 写入到 %rdi 碰巧指向的任何地方)并返回无效值(GCC 调用 Clang 编译的 @ 987654337@)。哪个编译器有问题?

相关:

另见

【问题讨论】:

  • 不是movl 32 位和movq 64 位!?
  • @DieterLücking:对于小于或等于UINT32_MAX的正整数常量无关紧要,寄存器的高32位被隐式设置为零。
  • @DieterLücking movl 指令清除 64 位目标寄存器中的其余位。只是指令编码比使用movq的等效指令短1个字节。
  • @jotik:缩短 2 个字节。 5 字节 movl $imm32, %r32 编码的 REX 版本是 10 字节 movabs $imm64, %r64(REX + 操作码 + 8 字节立即数)。符号扩展movq $imm32, %r/m64 形式为 7 个字节:它需要一个 mod/rm 字节,而不是将 dest 寄存器编码到操作码中。 (所以它可以存储到内存中,就像movq $2, (%rdi) 正在做的那样)

标签: c++ tuples x86-64 abi compiler-bug


【解决方案1】:

正如 davmac 的回答所示,libstdc++ std::tuple 可以简单地复制构造,但不能简单地移动构造。两个编译器在移动构造函数是否应该影响参数传递约定方面存在分歧。

您链接到的 C++ ABI 线程似乎解释了这种分歧: http://sourcerytools.com/pipermail/cxx-abi-dev/2016-February/002891.html

总之,Clang 完全实现了 ABI 规范所说的内容,但 G++ 实现了它应该说的内容,但实际上并未更新。

【讨论】:

  • 我可能遗漏了一些东西,但是,在您链接的线程中,建议的 ABI 文本是:在参数类型既没有平凡析构函数又至少有一个平凡的特殊情况下复制或移动未删除的构造函数,调用者必须为临时副本分配空间,并通过引用传递结果副本 - 这意味着tuple当然不需要通过不可见引用传递,因为它是微不足道的可移动和可破坏的? G++ 肯定不会遵循这一点。
  • (编辑:它是微不足道的可复制和可破坏的。)
  • 在重新阅读时,我在上面的评论中引用的文本是(IIUC)对“我们最终得到的规则”的建议改进(但从未进入规范),以及您所指的“应该说是后者而不是前者。好的,有道理。
【解决方案2】:

ABI 规定参数值是根据特定算法分类的。这里相关的是:

  1. 如果聚合的大小超过单个八字节,则将每个单独分类。每八字节被初始化为 NO_CLASS 类。

  2. 对对象的每个字段进行递归分类,以便始终考虑两个字段。根据八字节中字段的类别计算得到的类:

在这种情况下,每个字段(对于一个元组或一对)都是uint64_t 类型,因此占据了整个“八字节”。那么,每八字节中要考虑的“两个字段”是“NO_CLASS”(根据 3)和uint64_t 字段,它被归类为 INTEGER。

还有,与参数相关的传递

如果 C++ 对象具有非平凡的复制构造函数或非平凡的析构函数,则它通过不可见的引用传递(对象在参数列表中被具有类 INTEGER 的指针替换)

不满足这些要求的对象必须有地址,因此需要在内存中,这就是存在上述要求的原因。返回值也是如此,尽管这似乎在规范中被省略了(可能是偶然的)。

最后还有:

(c) 如果聚合的大小超过两个八字节并且前八字节不是 SSE 或任何其他八字节不是 SSEUP,则整个参数 在内存中传递。

这显然不适用于这里;聚合的大小正好是两个八字节。

在返回值时,文本说:

  1. 使用分类算法对返回类型进行分类

这意味着,如上所述,元组应归类为 INTEGER。那么:

  1. 如果类是 INTEGER,则序列的下一个可用寄存器 %rax, %rdx 被使用。

这很清楚。

唯一仍然悬而未决的问题是这些类型是否是非平凡复制可构造/可破坏的。如上所述,这种类型的值不能在寄存器中传递或返回,即使规范似乎没有认识到返回值的问题。但是,我们可以很容易地证明元组和对都是可平凡复制构造和可平凡破坏的,使用以下程序:

测试程序:

#include <utility>
#include <cstdint>
#include <tuple>
#include <iostream>

using namespace std;

int main(int argc, char **argv)
{
    cout << "pair is trivial? : " << is_trivial<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is trivially_copy_constructible? : " << is_trivially_copy_constructible<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is standard_layout? : " << is_standard_layout<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is pod? : " << is_pod<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is trivially_destructable? : " << is_trivially_destructible<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is trivially_move_constructible? : " << is_trivially_move_constructible<pair<uint64_t, uint64_t> >::value << endl;

    cout << "tuple is trivial? : " << is_trivial<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is trivially_copy_constructible? : " << is_trivially_copy_constructible<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is standard_layout? : " << is_standard_layout<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is pod? : " << is_pod<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is trivially_destructable? : " << is_trivially_destructible<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is trivially_move_constructible? : " << is_trivially_move_constructible<tuple<uint64_t, uint64_t> >::value << endl;
    return 0;
}

使用 GCC 或 Clang 编译时的输出:

pair is trivial? : 0
pair is trivially_copy_constructible? : 1
pair is standard_layout? : 1
pair is pod? : 0
pair is trivially_destructable? : 1
pair is trivially_move_constructible? : 1
tuple is trivial? : 0
tuple is trivially_copy_constructible? : 1
tuple is standard_layout? : 0
tuple is pod? : 0
tuple is trivially_destructable? : 1
tuple is trivially_move_constructible? : 0

这意味着 GCC 弄错了。返回值应该在 %rax,%rdx 中传递。

(类型之间的主要显着差异是 pair 是标准布局并且可以简单地移动构造,而 tuple 不是,因此 GCC 可能总是通过例如指针)。

【讨论】:

  • OP 链接到一个 cxx-abi-dev 线程,其中讨论了 move-constructors 对 ABI 的影响,并且同意应该考虑 trivially-move-constructible,因此 GCC 是 (我相信)为 libstdc++ std::tuple 做正确的事情。
猜你喜欢
  • 2018-04-08
  • 1970-01-01
  • 1970-01-01
  • 2012-10-25
  • 1970-01-01
  • 1970-01-01
  • 2011-02-21
  • 2022-01-14
  • 2015-08-20
相关资源
最近更新 更多