为什么std :: tuple破坏了C ++中的小型结构调用约定优化?

问题描述

C ++具有小型结构调用约定优化功能,其中,编译器通过函数参数传递小型结构的效率与传递原始类型(例如通过寄存器)的效率一样。例如:

class MyInt { int n; public: MyInt(int x) : n(x){} };
void foo(int);
void foo(MyInt);
void bar1() { foo(1); }
void bar2() { foo(MyInt(1)); }

bar1()bar2()生成几乎相同的汇编代码,除了分别调用foo(int)foo(MyInt)。特别是在x86_64上,它看起来像:

        mov     edi,1
        jmp     foo(MyInt) ;tail-call optimization jmp instead of call ret

但是如果我们测试std::tuple<int>,它将有所不同:

void foo(std::tuple<int>);
void bar3() { foo(std::tuple<int>(1)); }

struct MyIntTuple : std::tuple<int> { using std::tuple<int>::tuple; };
void foo(MyIntTuple);
void bar4() { foo(MyIntTuple(1)); }

生成的汇编代码看起来完全不同,小型结构(std::tuple<int>)通过指针传递:

        sub     rsp,24
        lea     rdi,[rsp+12]
        mov     DWORD PTR [rsp+12],1
        call    foo(std::tuple<int>)
        add     rsp,24
        ret

我挖得更深一些,试图使我的int更加脏(这应该接近一个不完整的朴素元组impl):

class Empty {};
class MyDirtyInt : protected Empty,MyInt {public: using MyInt::MyInt; };
void foo(MyDirtyInt);
void bar5() { foo(MyDirtyInt(1)); }

但应用了调用约定优化:

        mov     edi,1
        jmp     foo(MyDirtyInt)

我尝试了GCC / Clang / MSVC,它们都表现出相同的行为。 (Godbolt link here)所以我猜这一定是C ++标准中的东西吗? (我相信C ++标准没有指定任何ABI约束吗?)

我知道,只要foo(std::tuple<int>)的定义是可见的并且没有标记为noinline,编译器就应该能够对其进行优化。但是我想知道标准或实施的哪一部分导致此优化无效。

仅供参考,如果您对std::tuple的使用感到好奇,我想创建一个包装器类(即 strong typedef ),并且不想我自己声明比较运算符(运算符在C ++ 20之前),并且不想打扰Boost,所以我认为std::tuple一个很好的基类,因为所有内容都在那里。

解决方法

这似乎与ABI有关。例如,Itanium C++ ABI reads

如果出于调用目的参数类型非平凡,则调用方必须为临时变量分配空间并通过引用传递该临时变量。

然后,further

如果类型具有非平凡的副本构造函数,移动构造函数或析构函数或所有副本,则出于调用目的将其视为非平凡的和移动构造函数被删除。

AMD64 ABI Draft 1.0中的要求相同。

例如,在 libstdc ++ 中,std::tuple具有简单的移动构造函数:https://godbolt.org/z/4j8vds。该标准规定了both copy and move constructor as defaulted,在此满足。但是,同时tuple inherits from _Tuple_impl_Tuple_impl has a user-defined move constructor。因此,tuple本身的move构造函数不能太琐碎。

相反,在 libc ++ 中,std::tuple<int>的复制和移动构造函数都是微不足道的。因此,该参数在https://godbolt.org/z/WcTjM9的寄存器中传递。

对于 Microsoft STL std::tuple<int>既不可复制构造,也不可移动构造。甚至似乎违反了C ++标准规则。 std::tuple是递归定义的,在递归结束时,std::tuple<>专业化定义了non-defaulted copy constructor。关于此问题有一条评论:// TRANSITION,ABI: should be defaulted。由于tuple<>没有move构造函数,因此tuple<class...>的copy和move构造函数都是不平凡的。

,

如@StoryTeller所建议,它可能与std::tuple中用户定义的move构造函数有关,从而导致此行为。

例如参见:https://godbolt.org/z/3M9KWo

具有用户定义的move构造函数会导致未优化程序集:

bar_my_tuple():
        sub     rsp,24
        lea     rdi,[rsp+12]
        mov     DWORD PTR [rsp+12],1
        call    foo(MyTuple<int>)
        add     rsp,24
        ret

例如在libcxx中,复制和移动构造函数都被声明为默认for tuple_leaffor tuple,并且您获得了小型结构调用约定优化for std::tuple<int>但{{3} }持有一个不可微动的成员,因此自然就变得不可微动。