考虑以下代码:
#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
适合单个寄存器的生成代码为什么会更糟std::pair
?看起来特别奇怪,因为Clang 3和4似乎是最优的,而5则不是。
在这里试试:https://godbolt.org/g/T2Yqrj
发布于 2019-06-10 15:47:51
简短的回答是,因为libstc++
通过使用标准库的实现gcc
,并clang
在Linux上实现std::tuple
了不平凡的举动构造(尤其是_Tuple_impl
基类有一个不平凡的移动构造函数)。另一方面,复制和移动构造函数std::pair
都是默认的。
这反过来导致调用约定中的C ++ - ABI相关差异,用于从函数返回这些对象,以及通过值传递它们。
您在Linux上运行测试,该测试遵循SysV x86-64 ABI。此ABI具有将类或结构传递或返回到函数的特定规则,您可以在此处阅读更多信息。我们感兴趣的具体情况是int
这些结构中的两个字段是否会获得INTEGER
类或MEMORY
类。
一个最近的ABI规范的版本,有这样一段话:
聚合(结构和数组)和联合类型的分类如下:
条件(2)适用于此。请注意,它仅提及复制构造函数,而不是移动构造函数 - 但很明显,由于移动构造函数的引入通常需要包含在之前包含复制构造函数的任何分类算法中,因此很可能仅仅是规范中的缺陷。特别是IA-64 cxx-abi,其gcc
记录如下,确实包括移动构造函数:
如果参数类型对于调用而言是非平凡的,则调用者必须为临时值分配空间并通过引用传递该临时值。特别:
然后是非平凡的定义:
在以下情况下,对于呼叫,类型被认为是非平凡的:
因此,从ABI角度来看,它tuple
不被认为是可以轻易复制的,它会得到MEMORY
处理,这意味着你的函数必须填充被调用的传入的堆栈分配对象rdi
。该std::pair
函数可以只传回整个结构,rax
因为它适合于一个EIGHTBYTE
并具有类INTEGER
。
有关系吗?是的,严格地说,一个独立的功能,如你编译的功能将效率低,tuple
因为这个ABI不同的是“烘焙”。
但是,通常,即使没有内联,编译器也能够看到函数的主体并内联它或执行过程间分析。在这两种情况下,ABI不再重要,两种方法可能同样有效,至少对于一个不错的优化器。例如,让我们打电话给你f1()
和f2()
功能,并做结果一些数学:
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问题,而不是优化问题本身。无论铛和gcc已经优化的库代码以尽最大可能ABI的约束下-如果他们产生类似的代码f1()
的std::tuple
情况下,他们将打破ABI兼容的来电。
如果你切换到使用libc++
而不是Linux默认值,你可以清楚地看到这一点libstdc++
- 这个实现没有明确的移动构造函数(正如Marc Glisse在评论中提到的,他们坚持使用这个实现以实现向后兼容)。现在clang
(并且可能是gcc虽然我没有尝试过),但在两种情况下都会生成相同的最佳代码:
f1(): # @f1()
movabs rax, 2345052143889
ret
f2(): # @f2()
movabs rax, 2345052143889
ret
为什么版本的clang
编译方式不同?这只是铿锵声中的一个错误或规范中的一个错误,具体取决于你如何看待它。在需要传递临时指针的隐藏指针的情况下,规范没有明确包含移动构造。不符合IA-64 C ++ ABI。例如编译clang用来做它的方式不兼容gcc
或更新版本clang
。规范最终更新,版本5.0中的clang 行为发生了变化。
更新: Marc Glisse 在评论中提到最初对非平凡移动构造函数和C ++ ABI的交互存在混淆,并且clang
在某些时候改变了它们的行为,这可能解释了转换:
涉及移动构造函数的一些参数传递案例的ABI规范尚不清楚,当它们被澄清时,clang改为遵循ABI。这可能是其中一种情况。
https://stackoverflow.com/questions/-100005189
复制相似问题