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

uqcuzwp8  于 11个月前  发布在  其他
关注(0)|答案(2)|浏览(93)

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更脏一点(这应该接近一个不完整的naive tuple 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链接在这里)所以我猜这一定是C标准中的东西?(我相信C标准没有指定任何ABI约束,虽然?)
我知道只要foo(std::tuple<int>)的定义是可见的,并且没有标记为noinline,编译器就应该能够优化这些。但是我想知道标准或实现的哪一部分导致了这种优化的无效。
顺便说一下,如果你对我用std::tuple做什么感到好奇,我想创建一个 Package 器类(即 strong typedef),不想自己声明比较运算符(C++20之前的运算符<==>),也不想为Boost费心,所以我认为std::tuple是一个很好的基类,因为一切都在那里。
OP编辑:Daniel Langrpointed the root cause in the answer below。也请查看该答案下的评论。自从gcc 12.1.0发布以来,已经有a fix for this committed one year afterwards并合并到gcc,这已经过去了将近2年。

wecizke3

wecizke31#

这似乎是ABI的问题。例如,Itanium C++ ABI读取:
如果参数类型对于调用来说是非平凡的,调用者必须为临时变量分配空间,并通过引用传递临时变量。
此外,
如果一个类型有一个非平凡的复制构造函数、移动构造函数或析构函数,或者它的所有复制和移动构造函数都被删除,则该类型被认为是非平凡的。
AMD64 ABI Draft 1.0中也有相同的要求。
例如,在
libstdc++中,std::tuple有一个非平凡的移动构造函数:https://godbolt.org/z/4j8vds。标准规定了both copy and move constructor as defaulted,这里满足。但是,同时,tuple继承了_Tuple_impl_Tuple_impl有一个用户定义的移动构造函数。因此,tuple本身的移动构造函数不能是平凡的。
相反,在
libc++中,std::tuple<int>的复制和移动构造函数都是平凡的。因此,参数被传递到一个寄存器中:https://godbolt.org/z/WcTjM9
至于
Microsoft STL**,std::tuple<int>既不能复制构造,也不能移动构造。它甚至似乎违反了C++标准规则。std::tuple是递归定义的,在递归的最后,std::tuple<>专门化定义了非默认的复制构造函数。关于这个问题,有一条评论:// TRANSITION, ABI: should be defaulted。由于tuple<>没有移动构造函数,因此tuple<class...>的复制和移动构造函数都是非平凡的。

insrf1ej

insrf1ej2#

正如@StoryTeller所建议的那样,它可能与std::tuple内部导致此行为的用户定义的移动构造函数有关。
例如:https://godbolt.org/z/3M9KWo
拥有用户定义的移动构造函数会导致 * 非优化 * 的程序集:

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中,复制和移动构造函数被声明为tuple_leaftuple的默认构造函数,并且你得到了小型结构调用约定优化for std::tuple<int>,但not for std::tuple<std::string>拥有一个不可移动的成员,因此自然地变得不可移动。

相关问题