为什么 std::tuple 会破坏 C++ 中的小尺寸结构调用约定优化?

问题描述 投票:0回答:2

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 年后的事了。

c++ c++11 x86 calling-convention stdtuple
2个回答
13
投票

这似乎是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 STLstd::tuple<int>

 显然既不能复制构造也不能移动构造。它甚至似乎违反了 C++ 标准规则。 
std::tuple
 是递归定义的,并且在递归结束时, 
std::tuple<>
 特化定义了 
非默认复制构造函数。关于这个问题有一个评论:// TRANSITION, ABI: should be defaulted
。由于 
tuple<>
 没有移动构造函数,因此 
tuple<class...>
 的复制和移动构造函数都非常重要。


4
投票
正如 @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 中,复制和移动构造函数都被声明为默认值 

for tuple_leaf

for tuple
,并且您将获得小尺寸结构调用约定优化 
for std::tuple<int>
不适合std::tuple<std::string>
,它固定着一个不可平凡的可移动部件,因此本身自然变得不可平凡地移动。

© www.soinside.com 2019 - 2024. All rights reserved.