简短的回答是因为gcc 和clang 在Linux 上使用的libstc++ 标准库实现使用非平凡 移动构造函数实现std::tuple(特别是@ 987654336@ 基类有一个重要的移动构造函数)。另一方面,std::pair 的复制和移动构造函数都是默认的。
这反过来又会导致从函数返回这些对象以及按值传递它们的调用约定中与 C++-ABI 相关的差异。
血腥细节
您在遵循 SysV x86-64 ABI 的 Linux 上运行了测试。此 ABI 具有将类或结构传递或返回给函数的特定规则,您可以阅读有关 here 的更多信息。我们感兴趣的具体案例是这些结构中的两个int 字段是否将获得INTEGER 类或MEMORY 类。
ABI 规范的recent 版本有这样的说法:
聚合(结构体和数组)和联合的分类
类型的工作方式如下:
- 如果对象的大小大于 8 个八字节,或者它包含未对齐的字段,则它的类 MEMORY 12。
- 如果 C++ 对象具有非平凡的复制构造函数或非平凡的析构函数 13 ,则它通过不可见的引用(
参数列表中的对象被具有类的指针替换
整数)14 .
- 如果聚合的大小超过单个八字节,则将每个单独分类。每个八字节被初始化为类
NO_CLASS。
- 对象的每个字段都被递归分类,以便始终考虑两个字段。结果类是根据
到八字节中字段的类
这里适用的是条件(2)。请注意,它只提到了复制构造函数,而不是 move 构造函数 - 但很明显,这可能只是规范中的一个缺陷,因为引入了通常需要包含在任何分类中的移动构造函数之前包含复制构造函数的算法。特别是 IA-64 cxx-abi,gcc 被记录为遵循does include move constructors:
如果参数类型对于调用而言是非平凡的,则
调用者必须为临时分配空间并通过该临时传递
参考。具体来说:
然后是不平凡的definition:
如果满足以下条件,则类型被认为是非平凡的:
- 它有一个重要的复制构造函数、移动构造函数或析构函数,或者
- 它的所有复制和移动构造函数都被删除。
因此,因为从 ABI 的角度来看,tuple 不被认为是普通可复制,所以它得到了MEMORY 处理,这意味着您的函数必须填充被调用者传入的堆栈分配对象在rdi。 std::pair 函数可以将rax 中的整个结构传回,因为它适合一个EIGHTBYTE 并具有INTEGER 类。
这有关系吗?是的,严格来说,像您编译的那样的独立函数对于tuple 的效率会降低,因为这个 ABI 不同是“内置”的。
但是,即使没有内联,编译器通常也能看到函数体并内联它或执行过程间分析。在这两种情况下,ABI 都不再重要,而且很可能这两种方法都同样有效,至少在使用合适的优化器时是这样。例如let's call your f1() and f2() functions and do some math on the result:
int add_pair() {
auto p = f1();
return p.first + p.second;
}
int add_tuple() {
auto t = f2();
return std::get<0>(t) + std::get<1>(t);
}
原则上add_tuple方法从一个缺点开始,因为它必须调用f2(),效率较低,而且它还必须在堆栈上创建一个临时元组对象,以便它可以将它传递给f2隐藏参数。好吧,不管怎样,这两个函数都经过全面优化,可以直接返回正确的值:
add_pair():
mov eax, 819
ret
add_tuple():
mov eax, 819
ret
因此,总的来说,您可以说 tuple 的这个 ABI 问题的影响相对较小:它为必须符合 ABI 的函数增加了一小部分固定开销,但这仅在相对意义上真正重要非常小的函数 - 但这些函数很可能在可以内联的地方声明(或者如果没有,你将把性能留在桌面上)。
libcstc++ 与 libc++++
如上所述,这本身是一个 ABI 问题,而不是优化问题。 clang 和 gcc 都已经在 ABI 的约束下尽可能地优化库代码 - 如果他们为 std::tuple 情况生成像 f1() 这样的代码,他们将破坏符合 ABI 的调用者。
如果您切换到使用 libc++ 而不是 Linux 默认的 libstdc++,您可以清楚地看到这一点 - 此实现没有显式移动构造函数(正如 Marc Glisse 在 cmets 中提到的那样,他们坚持使用这个实现向后兼容)。现在clang(可能是gcc,虽然我没有尝试过),在这两种情况下都会生成same optimal code:
f1(): # @f1()
movabs rax, 2345052143889
ret
f2(): # @f2()
movabs rax, 2345052143889
ret
Clang 的早期版本
为什么clang 的版本编译不同?这只是a bug in clang 或规范中的错误,具体取决于您如何看待它。在需要传递指向临时对象的隐藏指针的情况下,规范没有明确包括移动构造。不符合 IA-64 C++ ABI。例如,按照 clang 的方式编译它与 gcc 或更新版本的 clang 不兼容。规范是eventually updated 和clang behavior changed in version 5.0。
更新: Marc Glisse mentions 在 cmets 中最初对非平凡移动构造函数和 C++ ABI 的交互感到困惑,clang 在某些时候改变了它们的行为,这可能解释了开关:
一些涉及移动的参数传递案例的 ABI 规范
构造函数不清楚,当他们澄清时,clang改变了
遵循 ABI。这可能是其中一种情况。