C++ 返回2元组的效率是否低于std::pair?
考虑以下代码:C++ 返回2元组的效率是否低于std::pair?,c++,gcc,clang,calling-convention,stdtuple,C++,Gcc,Clang,Calling Convention,Stdtuple,考虑以下代码: #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 5为f2()生成了不同的代码: 与GCC 4至GCC 7一
#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 5为f2()生成了不同的代码:
与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
相比,生成的代码更糟糕?这似乎特别奇怪,因为叮当3和4似乎是最佳的,而5不是
在这里试试:简单的答案是因为libstc++
标准库实现由gcc
和clang
在Linux上使用非平凡移动构造函数实现std::tuple
(特别是\u tuple\u impl
基类具有非平凡移动构造函数)。另一方面,std::pair
的复制和移动构造函数都是默认的
这反过来会导致调用约定中与C++-ABI相关的差异,用于从函数返回这些对象以及按值传递它们
血淋淋的细节
您在遵循SysV x86-64 ABI的Linux上运行了测试。此ABI具有向函数传递或返回类或结构的特定规则,您可以详细了解这些规则。我们感兴趣的具体情况是,这些结构中的两个int
字段将获得INTEGER
类还是MEMORY
类
ABI规范的一个版本说:
聚合(结构和数组)和并集的分类
工作类型如下:
如果一个对象的大小大于8个8字节,或者它包含未对齐的字段,那么它的类内存为12
<>如果C++对象有非平凡的拷贝构造函数或非平凡的析构函数13,则它是由不可见引用传递的。
对象在参数列表中被具有类的指针替换
整数)14
如果骨料的尺寸超过一个八字节,则每个八字节单独分类。每个八字节初始化为类
没有课
对象的每个字段都被递归地分类,以便始终考虑两个字段。生成的类是根据
到八字节中字段的类
这里适用的是条件(2)。请注意,它只提到复制构造函数,而没有提到移动构造函数——但很明显,由于引入了移动构造函数,这可能只是规范中的一个缺陷,而移动构造函数通常需要包含在以前包含复制构造函数的任何分类算法中。特别是IA-64 cxx abi,其gcc
记录如下:
如果参数类型对于调用而言是非平凡的,则
调用方必须为临时文件分配空间,并通过
参考资料。具体而言:
- 调用者以通常的方式为临时文件(通常在堆栈上)分配空间
然后是非平凡的问题:
在以下情况下,就调用而言,类型被认为是非平凡的:
- 它有一个非平凡的复制构造函数、移动构造函数或析构函数,或
- 将删除其所有复制和移动构造函数
因此,因为从ABI的角度来看,tuple
不被认为是可复制的,所以它得到了MEMORY
处理,这意味着您的函数必须填充由调用的rdi
传入的堆栈分配对象。std::pair
函数可以在rax
中传回整个结构,因为它适合于一个EIGHTBYTE
,并且具有类INTEGER
这有关系吗?是的,严格地说,像您编译的那样的独立函数对于tuple
来说效率较低,因为这个ABI是“烘焙的”
但是,编译器通常能够看到函数体并将其内联,或者执行过程间分析,即使没有内联。在这两种情况下,ABI不再重要,而且这两种方法可能同样有效,至少在有一个合适的优化器的情况下是如此。例如:
因此,总体而言,您可以说,这个ABI问题对tuple
的影响将相对减弱:它为必须符合ABI的函数增加了一小部分固定开销,但这只会在相对意义上对非常小的函数起作用-但这类函数很可能是在可以内联的位置声明的(或者,如果不是这样,你将把表现留在桌面上)
libcstc++与libc+++
如上所述,这本身是ABI问题,而不是优化问题。在ABI的约束下,clang和gcc都已经在尽可能最大程度地优化库代码-如果他们为std::tuple
生成类似f1()
的代码,他们将破坏符合ABI的调用方
如果您切换到使用libc++
而不是Linux默认的libstdc++
,您可以清楚地看到这一点-此实现没有显式的移动构造函数(正如Marc Glisse在评论中提到的,为了向后兼容,他们一直使用此实现)。现在clang
(可能是gcc,尽管我没有尝试),在这两种情况下都会生成:
f1(): # @f1()
movabs rax, 2345052143889
ret
f2(): # @f2()
movabs rax, 2345052143889
ret
Clang的早期版本
< > >为什么代码版本> CLAN/<代码>编译它不同?它只是简单的或是C++中的一个bug,取决于你如何看待它。在一个临时指针需要被传递的情况下,规范没有明确地包含移动构造。与gcc
或更新版本的clang
不兼容。规范为和clang
更新:Marc Glisse在评论中说,最初对非琐碎的移动公司的互动感到困惑
f2():
movabs rdx,0x11100000222
mov rax,rdi
mov QWORD PTR [rdi],rdx ; GCC 4-6 use 2 DWORD stores
ret
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_pair():
mov eax, 819
ret
add_tuple():
mov eax, 819
ret
f1(): # @f1()
movabs rax, 2345052143889
ret
f2(): # @f2()
movabs rax, 2345052143889
ret