GCC/Clang x86_64 返回元组时 C++ ABI 不匹配?

GCC/Clang x86_64 C++ ABI mismatch when returning a tuple?

尝试 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 outputs 此汇编代码为 f:

movl , %eax
movl , %edx
retq

这是 g:

movl , %eax
movl , %edx
retq

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

movq %rdi, %rax
movq , (%rdi)
movq , 8(%rdi)
ret

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

相关:

另见

ABI 声明参数值根据特定算法进行分类。相关的是:

  1. If the size of the aggregate exceeds a single eightbyte, each is classified separately. Each eightbyte gets initialized to class NO_CLASS.

  2. Each field of an object is classified recursively so that always two fields are considered. The resulting class is calculated according to the classes of the fields in the eightbyte:

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

还有,参数相关passing:

If a C++ object has either a non-trivial copy constructor or a non-trivial destructor, it is passed by invisible reference (the object is replaced in the parameter list by a pointer that has class INTEGER)

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

最后还有:

(c) If the size of the aggregate exceeds two eightbytes and the first eight-byte isn’t SSE or any other eightbyte isn’t SSEUP, the whole argument is passed in memory.

显然,这不适用于此处;聚合的大小恰好是两个八字节。

关于 returning 值,文本说:

  1. Classify the return type with the classification algorithm

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

  1. If the class is INTEGER, the next available register of the sequence %rax, %rdx is used.

这个很清楚了。

唯一仍然悬而未决的问题是类型是否 non-trivially-copy-constructible/destructible。如上所述,这种类型的值不能在寄存器中传递或 returned,即使规范似乎没有识别 return 值的问题。但是,我们可以使用以下程序轻松证明元组和对都是可平凡复制构造和平凡可破坏的:

测试程序:

#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 弄错了。 return 值应在 %rax,%rdx 中传递。

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

正如 davmac 的回答所示,libstdc++ std::tuple 是平凡复制可构造的,但不是平凡移动可构造的。两个编译器不同意移动构造函数是否应该影响参数传递约定。

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

总而言之,Clang 实现了 ABI 规范所说的,但 G++ 实现了应该说的,但没有更新为实际说的。