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 更脏一些(这应该接近于不完整的朴素元组实现):
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
做什么感到好奇,我想创建一个包装类(即strong typedef)并且不想声明比较运算符(operator<==>的先前到 C++20)我自己并且不想打扰 Boost,所以我认为 std::tuple
是一个很好的基类,因为一切都在那里。
OP的编辑: Daniel Langr 在下面的答案中指出了根本原因。另请检查该答案下的评论。 并且已经有在一年后提交了对此的修复,并自 gcc 12.1.0 发布以来合并到 gcc,这已经是近 2 年后的事了。
这似乎是ABI的问题。例如,Itanium C++ ABI 为:
如果参数类型并且,对于调用而言非常重要,则调用者必须为临时对象分配空间并通过引用传递该临时对象。
进一步:
如果某个类型具有非平凡的复制构造函数、移动构造函数或析构函数,或者其所有复制和移动构造函数都被删除,则该类型被视为对于调用而言是非平凡的。
AMD64 ABI 草案 1.0 中也有相同的要求。
例如,在libstdc++ 中,std::tuple
有重要的移动构造函数:https://godbolt.org/z/4j8vds。标准规定复制和移动构造函数都是默认的,这里满足了。然而,同时,
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...>
的复制和移动构造函数都非常重要。
std::tuple
内用户定义的移动构造函数有关。参见示例:用户定义的移动构造函数会导致
非优化程序集:
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 中,复制和移动构造函数都被声明为默认值 for tuple_leaf
tuple
,并且您将获得小尺寸结构调用约定优化 for std::tuple<int>
但 不适合std::tuple<std::string>
,它固定着一个不可平凡的可移动部件,因此本身自然变得不可平凡地移动。