返回比std :: pair更低效的2元组?

时间:2017-10-24 03:24:19

标签: 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 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不是。

在此处试试:https://godbolt.org/g/T2Yqrj

1 个答案:

答案 0 :(得分:16)

简短的回答是因为libstc++gcc在Linux上使用的clang标准库实现使用非平凡来实现std::tuple移动构造函数(特别是,_Tuple_impl基类有一个非平凡的移动构造函数)。另一方面,std::pair的复制和移动构造函数都是默认的。

血腥细节

您在Linux上运行测试,该测试遵循SysV x86-64 ABI。此ABI具有将类或结构传递或返回到函数的特定规则,您可以阅读有关here的更多信息。我们感兴趣的具体案例是这些结构中的两个int字段是否会获得INTEGER类或MEMORY类。

ABI规范的recent版本可以这样说:

  

聚合(结构和数组)和联合的分类   类型的工作原理如下:

     
      
  1. 如果一个对象的大小大于八个八字节,或者它包含未对齐的字段,则它具有MEMORY 12类。
  2.   
  3. 如果C ++对象具有非平凡的复制构造函数或非平凡的析构函数13,则它由不可见的引用传递(   对象在参数列表中被具有类的指针替换   INTEGER)14。
  4.   
  5. 如果聚合体的大小超过单个八字节,则每个都单独分类。每个八字节都被初始化为类   NO_CLASS。
  6.   
  7. 对象的每个字段都是递归分类的,因此始终考虑两个字段。得到的类是根据计算的   到八字节的字段类
  8.   

这里适用条件(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的函数增加了一个小的固定开销,但这只会在一个亲戚中真正重要对于非常小的函数有意义 - 但是这些函数很可能在它们可以被内联的地方声明(或者如果没有,你就会把性能留在桌面上)。

libcs​​tc ++ vs libc +++

如上所述,这是一个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的早期版本

为什么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。这可能是其中一种情况。