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

Yum*_*Yao 29 c++ x86 calling-convention c++11 stdtuple

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)); }
Run Code Online (Sandbox Code Playgroud)

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

        mov     edi, 1
        jmp     foo(MyInt) ;tail-call optimization jmp instead of call ret
Run Code Online (Sandbox Code Playgroud)

但是如果我们测试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)); }
Run Code Online (Sandbox Code Playgroud)

生成的汇编代码看起来完全不同,小尺寸的struct( 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
Run Code Online (Sandbox Code Playgroud)

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

class Empty {};
class MyDirtyInt : protected Empty, MyInt {public: using MyInt::MyInt; };
void foo(MyDirtyInt);
void bar5() { foo(MyDirtyInt(1)); }
Run Code Online (Sandbox Code Playgroud)

但应用了调用约定优化:

        mov     edi, 1
        jmp     foo(MyDirtyInt)
Run Code Online (Sandbox Code Playgroud)

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

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

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

Dan*_*ica 11

这似乎是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...>都是重要的。

  • @MaximEgorushkin甚至似乎已经提出了[相关补丁](https://patchwork.ozlabs.org/project/gcc/patch/alpine.DEB.2.02.1605232038220.30609@laptop-mg.saclay.inria.fr/) ,但由于破坏了有关调用约定的向后兼容性而未被接受。 (3认同)
  • ABI 稳定性原教旨主义者再次毁掉了 C++。:-( (3认同)

Ami*_*rsh 4

正如 @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
Run Code Online (Sandbox Code Playgroud)

例如,在 libcxx 中,复制和移动构造函数都被声明为fortuple_leaffortuple的默认值,并且您将获得小尺寸结构调用约定优化forstd::tuple<int>不是 forstd::tuple<std::string>,其中包含非平凡可移动成员,因此本身自然变得非平凡可移动。