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 STL,std::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
Run Code Online (Sandbox Code Playgroud)
例如,在 libcxx 中,复制和移动构造函数都被声明为fortuple_leaf和fortuple的默认值,并且您将获得小尺寸结构调用约定优化forstd::tuple<int>但不是 forstd::tuple<std::string>,其中包含非平凡可移动成员,因此本身自然变得非平凡可移动。