为啥 std::optional<int> 的构造比 std::pair<int, bool> 更昂贵?

Posted

技术标签:

【中文标题】为啥 std::optional<int> 的构造比 std::pair<int, bool> 更昂贵?【英文标题】:Why is the construction of std::optional<int> more expensive than a std::pair<int, bool>?为什么 std::optional<int> 的构造比 std::pair<int, bool> 更昂贵? 【发布时间】:2018-03-14 14:48:25 【问题描述】:

考虑这两种可以表示“可选int”的方法:

using std_optional_int = std::optional<int>;
using my_optional_int = std::pair<int, bool>;

鉴于这两个功能...

auto get_std_optional_int() -> std_optional_int 

    return 42;


auto get_my_optional() -> my_optional_int 

    return 42, true;

...g++ trunkclang++ trunk (使用 -std=c++17 -Ofast -fno-exceptions -fno-rtti 生成以下程序集:

get_std_optional_int():
        mov     rax, rdi
        mov     DWORD PTR [rdi], 42
        mov     BYTE PTR [rdi+4], 1
        ret

get_my_optional():
        movabs  rax, 4294967338 // == 0x 0000 0001 0000 002a
        ret

live example on godbolt.org


为什么get_std_optional_int() 需要三个mov 指令,而get_my_optional() 只需要一个movabs 这是一个QoI 问题,还是std::optional 的问题?规范阻止了这种优化?

另外请注意,这些功能的用户可能会被完全优化出来:

volatile int a = 0;
volatile int b = 0;

int main()

    a = get_std_optional_int().value();
    b = get_my_optional().first;

...结果:

main:
        mov     DWORD PTR a[rip], 42
        xor     eax, eax
        mov     DWORD PTR b[rip], 42
        ret

【问题讨论】:

optional 通过隐藏指针返回,这意味着类型定义包含禁止通过寄存器返回的内容。 明显的区别是std::pair是一个聚合,而std::optional不是。不知道它是否应该有效果,但你知道... boost::optional 相同的问题,在任何版本的 GCC 上,演示都不需要花哨的 C++17:godbolt.org/g/MV14mr 聚合与非聚合类型、SYS V x64 ABI 以及 4294967338 是 0x10000002a 的事实应该清楚地说明这一点。 @WojciechMigda folly::Optional 没有必要的魔法来使其特殊的成员函数有条件地变得微不足道。 (它还通过在内联函数中使用内部链接 None 违反了 ODR,并且每个 constexprFOLLY_CPP14_CONSTEXPR 函数都是格式错误的 NDR:您不能使用 optionalconstexpr API aligned_storage.) +1 为 co_await-able,但他们最好从 range-v3 窃取 optional 实现并添加其 API 的其余部分。 【参考方案1】:

libstdc++ 显然没有实现P0602 "variant and optional should propagate copy/move triviality"。您可以通过以下方式验证这一点:

static_assert(std::is_trivially_copyable_v<std::optional<int>>);

which fails for libstdc++, and passes for libc++ and the MSVC standard library(它确实需要一个合适的名称,因此我们不必将其称为“C++ 标准库的 MSVC 实现”或“MSVC STL”)。

当然,MSVC 仍然不会在寄存器中传递optional&lt;int&gt;,因为 MS ABI。

编辑:此问题已在 GCC 8 发行系列中得到修复。

【讨论】:

optional 仍然需要一个析构函数,这会阻止它在寄存器中返回。 @MaximEgorushkin 只有在 T 有析构函数时才需要析构函数。 那篇论文被采纳了吗?它没有反映在latest draft 中。 @Barry Per this 它现在在 Library Evolution 中。 正确,P0602 尚未采用。作者尝试与主要实现者进行沟通,以便在人们发布和锁定 ABI 之前“修复”optionalvector。我相信 libstdc++/libc++/MSFT variant 都符合要求,libc++/MSFT optional 也是如此,但显然 libstdc++ optional 的维护者没有得到备忘录。【参考方案2】:

为什么get_std_optional_int() 需要三个mov 指令,而 get_my_optional() 只需要一个movabs

直接原因是optional是通过隐藏指针返回的,而pair是在寄存器中返回的。为什么呢? SysV ABI 规范,3.2.3 参数传递部分说:

如果 C++ 对象具有非平凡的复制构造函数或 非平凡的析构函数,它通过不可见的引用传递。

整理出optional这个C++乱七八糟的东西并不容易,但似乎有一个non-trivial copy constructor at least in the optional_base class of the implementation I checked。

【讨论】:

不一定是,但可能是,这对编译器很重要。所以你实际上必须看看实现。【参考方案3】:

在Calling conventions for different C++ compilers and operating systems by Agner Fog 中,它说复制构造函数或析构函数阻止返回寄存器中的结构。这就解释了为什么optional 没有在寄存器中返回。

必须有其他东西阻止编译器进行存储合并(将比单词更窄的立即值的连续存储合并到更少的更宽存储中以减少指令数量)... 更新:gcc bug 82434 - -fstore-merging does not work reliably.

【讨论】:

知道这个“其他东西”可能是什么吗? x86-64 没有mov mem, imm64,因此您无法合并商店。 @Jester 它可以使用合并的值加载rax 并将其存储到[rdi] 当然可以,但我不认为这有任何固有的速度优势,而且这是更短的机器代码。它当然不会减少指令的数量。 @Maxim:gcc7 实现了存储合并(gcc3 或 gcc4 破坏了,所以 years 的 gcc 已经在相邻的窄分配中陷入困境),但是在对象中仍然有一个错过的优化填充。 gcc.gnu.org/bugzilla/show_bug.cgi?id=82142。 (在这种情况下,clang 似乎有相同的优化缺失。)如果您编写一个通过 pair&lt;int,int&gt; 指针存储的函数,gcc 和 clang 将合并存储:godbolt.org/g/44zodQ。【参考方案4】:

优化在技术上是允许的,即使std::is_trivially_copyable_v&lt;std::optional&lt;int&gt;&gt; 为假。但是,它可能需要编译器找到不合理程度的“聪明”。此外,对于使用std::optional 作为函数返回类型的特定情况,可能需要在链接时而不是编译时进行优化。

执行此优化不会影响任何(定义明确的)程序的可观察行为*,因此在as-if rule 下隐含允许。但是,由于其他答案中解释的原因,编译器尚未明确意识到这一事实,需要从头开始推断。行为静态分析是inherently difficult,所以编译器可能无法证明这种优化在所有情况下都是安全的。

假设编译器可以找到这个优化,那么它就需要改变这个函数的调用约定(即改变函数返回给定值的方式),这通常需要在链接时完成,因为调用约定会影响所有呼叫站点。或者,编译器可以完全内联函数,这在编译时可能会也可能不会。对于可简单复制的对象,这些步骤不是必需的,因此从这个意义上说,该标准确实抑制了优化并使优化复杂化。

std::is_trivially_copyable_v&lt;std::optional&lt;int&gt;&gt; 应该是真的。如果这是真的,编译器会更容易发现和执行这种优化。所以,回答你的问题:

这是一个 QoI 问题,还是 std::optional 的规范中的某些内容阻止了这种优化?

两者兼而有之。该规范使优化变得更难找到,并且实现不够“智能”,无法在这些约束下找到它。


* 假设你没有做过什么很奇怪的事情,比如#define int something_else

【讨论】:

您将调用约定作为“实现”的一部分。这在技术上是正确的,但除非您启用整个程序/链接时优化,否则给定平台上的编译器甚至不会尝试更改,而不是通过完全内联或制作恒定传播克隆版本的函数。 我猜 gcc 可能会发出类似.gcc_reg_return_get_std_optional_int.clone123 的东西以及符合 ABI 标准的正常定义。但是,来自其他翻译单位的调用者无法假设存在这种情况,因此他们必须调用常规版本(除非您使用 LTO,在这种情况下它只是内联,因为它很小)。但如果该函数实际上很大,那么确保克隆该函数的备用调用约定版本会很有用。在 2 个单独的 regs 中返回部件可能最有用,而不是打包到 RAX 中。 @PeterCordes:你确定吗? function 的实现在这里似乎完全无关紧要,只有std::optional 的实现是必要的,因为它是一个模板,所以它的实现总是可用的。 @MatthieuM.: 是的,我相信 gcc 和 clang 正在遵循他们决定使用的 C++ ABI,其中 返回时如果足够小,则打包到寄存器中的标准或传值是std::is_trivially_copyable_v&lt;foo&gt;。对于存在析构函数但已知要优化掉的模板类的情况,您可能会更改 C++ ABI 以实现更复杂的规则,或者其他什么。但这可能需要始终启用优化,以便编译器就如何传递某些对象达成一致。 (例如内联和优化。) C++ ABI 并不像 C ABI / 调用约定 (x86-64 System V psABI) 那样稳定,但不同的编译器必须能够可靠地相互一致。

以上是关于为啥 std::optional<int> 的构造比 std::pair<int, bool> 更昂贵?的主要内容,如果未能解决你的问题,请参考以下文章

使用 std::optional 通过引用将 std::vector<int> 传递给函数

为啥 std::optional 构造函数使用 std::in_place?

为啥 std::optional::value() &&;返回 &&?

为啥 const rvalue 限定 std::optional::value() 返回 const rvalue 引用?

为啥 std::optional 对 std::nullopt 类型的操作数有一个特殊的相等运算符

如何在 Xcode 中获得 std::optional 支持?