C++有一个小型结构体调用约定优化,编译器以与传递原始类型(例如,通过寄存器)相同的效率传递函数参数中的小型结构体。例如:
但是如果我们测试std::tuple,结果会不同:
生成的汇编代码看起来完全不同,小型结构体(
我深入挖掘了一些,试图让我的整数变得更加"脏"一些(这应该接近于一个不完整的天真元组实现):
但是调用约定优化被应用:
我尝试过GCC/Clang/MSVC,它们都显示了相同的行为。(这里是Godbolt链接)所以我猜这一定是C++标准中的某个问题?(虽然我相信C++标准没有规定任何ABI约束,对吗?)
我知道编译器应该能够优化掉这些,只要
OP的编辑: Daniel Langr在下面的答案中指出了根本原因。请还要查看该答案下的评论。 而且,自gcc 12.1.0发布以来,已经有一年后提交的修复方案并合并到gcc中,这已经过去了将近2年的时间。
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,结果会不同:
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
我深入挖掘了一些,试图让我的整数变得更加"脏"一些(这应该接近于一个不完整的天真元组实现):
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
的目的感兴趣,我想创建一个包装类(即强类型定义),并且不想自己声明比较运算符(在C++20之前的operator<==>),也不想麻烦使用Boost,所以我觉得std::tuple
是一个很好的基类,因为里面包含了所有需要的东西。
OP的编辑: Daniel Langr在下面的答案中指出了根本原因。请还要查看该答案下的评论。 而且,自gcc 12.1.0发布以来,已经有一年后提交的修复方案并合并到gcc中,这已经过去了将近2年的时间。
std::tuple<int>
的析构函数应该是平凡的。 - Konrad RudolphMyInt
中具有相同的效果:https://godbolt.org/z/s4zzcx。 - Daniel Langr