繁体   English   中英

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

[英]Why does std::tuple break small-size struct calling convention optimization in 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 链接)所以我猜这一定是 C++ 标准中的东西? (不过,我相信 C++ 标准没有指定任何 ABI 约束?)

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

仅供参考,如果您对我正在用std::tuple做什么感到好奇,我想创建一个包装类(即强 typedef )并且不想声明比较运算符(运算符<==>的在 C++20 之前)我自己并且不想打扰 Boost,所以我认为std::tuple是一个很好的基类,因为一切都在那里。

这似乎是ABI的问题。 例如,安腾 C++ ABI 读取

如果参数类型对于调用而言是重要的,则调用者必须为临时文件分配空间并通过引用传递该临时文件。

而且, 进一步

如果一个类型具有非平凡的复制构造函数、移动构造函数或析构函数,或者它的所有复制和移动构造函数都被删除则就调用而言,该类型被认为是非平凡的

相同的要求在AMD64 ABI Draft 1.0 中

例如,在libstdc++ 中std::tuple具有非平凡的移动构造函数: https : //godbolt.org/z/4j8vds 标准将复制和移动构造函数都规定为 defaulted ,这在此处得到满足。 但是,与此同时, tuple 继承自_Tuple_impl并且_Tuple_impl具有用户定义的移动构造函数 结果, tuple本身的移动构造函数不能是微不足道的。

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

至于Microsoft STLstd::tuple<int>既不是可复制构造的,也不是可移动构造的。 它甚至似乎违反了 C++ 标准规则。 std::tuple是递归定义的,并且在递归结束时, std::tuple<>定义了非默认复制构造函数 有关于这个问题的评论: // TRANSITION, ABI: should be defaulted 由于tuple<>没有移动构造函数,因此tuple<class...>复制和移动构造函数都非常重要。

正如@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默认构造函数,并且您获得了std::tuple<int>的小尺寸结构调用约定优化不适用于std::tuple<std::string>持有一个非平凡可移动的成员,因此它自己自然变得不可平凡移动。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM