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 Langr有pointed the root cause in the answer below。也请查看该答案下的评论。自从gcc 12.1.0发布以来,已经有a fix for this committed one year afterwards并合并到gcc,这已经过去了将近2年。
2条答案
按热度按时间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...>
的复制和移动构造函数都是非平凡的。insrf1ej2#
正如@StoryTeller所建议的那样,它可能与
std::tuple
内部导致此行为的用户定义的移动构造函数有关。例如:https://godbolt.org/z/3M9KWo
拥有用户定义的移动构造函数会导致 * 非优化 * 的程序集:
字符串
例如,在libcxx中,复制和移动构造函数被声明为
tuple_leaf
和tuple
的默认构造函数,并且你得到了小型结构调用约定优化forstd::tuple<int>
,但not forstd::tuple<std::string>
拥有一个不可移动的成员,因此自然地变得不可移动。