考虑以下代码:
#include <utility>
#include <tuple>
std::pair<int, int> f1()
{
return std::make_pair(0x111, 0x222);
}
std::tuple<int, int> f2()
{
return std::make_tuple(0x111, 0x222);
}
Clang 3和4在x86-64上生成类似的代码:
f1():
movabs rax,0x22200000111
ret
f2():
movabs rax,0x11100000222 ; opposite packing order, not important
ret
但是Clang 5为f2()
生成了不同的代码:
f2():
movabs rax,0x11100000222
mov QWORD PTR [rdi],rax
mov rax,rdi
ret
GCC 4至GCC 7:
f2():
movabs rdx,0x11100000222
mov rax,rdi
mov QWORD PTR [rdi],rdx ; GCC 4-6 use 2 DWORD stores
ret
为什么返回适合单个寄存器std::tuple
的{{1}}时生成的代码会更糟?看起来特别奇怪,因为Clang 3和4似乎是最优的,但是5不是。
答案 0 :(得分:16)
简短的回答是因为libstc++
和gcc
在Linux上使用的clang
标准库实现使用非平凡来实现std::tuple
移动构造函数(特别是,_Tuple_impl
基类有一个非平凡的移动构造函数)。另一方面,std::pair
的复制和移动构造函数都是默认的。
您在Linux上运行测试,该测试遵循SysV x86-64 ABI。此ABI具有将类或结构传递或返回到函数的特定规则,您可以阅读有关here的更多信息。我们感兴趣的具体案例是这些结构中的两个int
字段是否会获得INTEGER
类或MEMORY
类。
ABI规范的recent版本可以这样说:
聚合(结构和数组)和联合的分类 类型的工作原理如下:
- 如果一个对象的大小大于八个八字节,或者它包含未对齐的字段,则它具有MEMORY 12类。
- 如果C ++对象具有非平凡的复制构造函数或非平凡的析构函数13,则它由不可见的引用传递( 对象在参数列表中被具有类的指针替换 INTEGER)14。
- 如果聚合体的大小超过单个八字节,则每个都单独分类。每个八字节都被初始化为类 NO_CLASS。
- 对象的每个字段都是递归分类的,因此始终考虑两个字段。得到的类是根据计算的 到八字节的字段类
醇>
这里适用条件(2)。请注意,它仅提及复制构造函数,而不是移动构造函数 - 但很明显,由于移动构造函数的引入通常需要包含在任何分类中,因此很可能只是规范中的缺陷之前包含复制构造函数的算法。特别是,IA-64 cxx-abi,gcc
被记录为遵循does include move constructors:
如果参数类型对于调用而言是非平凡的,那么 调用者必须为临时空间分配空间并将其临时传递 参考。具体做法是:
- 调用者以通常的方式为临时空间分配空间,通常在堆栈上。
然后是非平凡的definition:
如果出现以下情况,则类型被视为非常重要:
- 它有一个非平凡的复制构造函数,移动构造函数或析构函数,或
- 删除所有复制和移动构造函数。
因此,从ABI角度来看,tuple
不被认为是平易可复制的,它会得到MEMORY
处理,这意味着你的函数必须填充堆栈分配的对象传递在rdi
中被叫。 std::par
函数可以传回rax
中的整个结构,因为它适合一个EIGHTBYTE
并且具有类INTEGER
。
重要吗?是的,严格来说,像你编译的那个独立函数对tuple
的效率会降低,因为这个ABI不同的是&#34;在&#34;中烘烤。
然而,通常,即使没有内联,编译器也能够看到函数的主体并内联它或执行过程间分析。在这两种情况下,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
总的来说,你可以说这个ABI问题与tuple
的影响相对较小:它为必须符合ABI的函数增加了一个小的固定开销,但这只会在一个亲戚中真正重要对于非常小的函数有意义 - 但是这些函数很可能在它们可以被内联的地方声明(或者如果没有,你就会把性能留在桌面上)。
如上所述,这是一个ABI问题,而不是优化问题本身。 clang和gcc都已经在ABI的约束下最大程度地优化了库代码 - 如果他们为f1()
生成代码std::tuple
,它们将破坏符合ABI的调用者。
如果您切换到使用libc++
而不是Linux默认值libstdc++
,您可以清楚地看到这一点 - 此实现没有明确的移动构造函数(正如Marc Glisse在评论中提到的那样) ,他们坚持使用此实现以实现向后兼容性)。现在clang
(可能是gcc,虽然我没有尝试过),但在两种情况下都会生成same optimal code:
f1(): # @f1()
movabs rax, 2345052143889
ret
f2(): # @f2()
movabs rax, 2345052143889
ret
为什么clang
的版本以不同方式编译?它只是a bug in clang或规范中的错误,具体取决于您如何看待它。在需要传递临时指针的隐藏指针的情况下,规范没有明确包含移动构造。不符合IA-64 C ++ ABI。例如,编译clang用于执行此操作的方式与gcc
或更新版本的clang
不兼容。规范是eventually updated和clang behavior changed in version 5.0。
更新: Marc Glisse mentions在评论中最初混淆了非平凡移动构造函数与C ++ ABI的交互,clang
改变了他们的行为在某些时候,这可能解释了转变:
针对涉及移动的一些参数传递案例的ABI规范 施工人员不清楚,当他们澄清时,铿锵声改变了 遵循ABI。这可能是其中一种情况。