![](/img/trans.png)
[英]Why does c++ std::hash create a functor struct and can it be called without creating a struct each time
[英]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 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
例如,在 libcxx 中,复制和移动构造函数被声明为tuple_leaf
和tuple
默认构造函数,并且您获得了std::tuple<int>
的小尺寸结构调用约定优化,但不适用于std::tuple<std::string>
持有一个非平凡可移动的成员,因此它自己自然变得不可平凡移动。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.