为什么 std: : 可选 < int > 的构造比 std: : double < int,bool > 更昂贵?

考虑这两种可以表示“可选 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};
}

... 后备箱叮当作响的树干 (连同 -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

在 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
5098 次浏览

Agner Fog 为不同的 C + + 编译器和操作系统调用约定中,复制建构子或析构函数阻止返回寄存器中的结构。这解释了为什么不在寄存器中返回 optional

肯定还有其他东西阻止编译器进行存储合并(为了减少指令的数量,将比一个单词更窄的直接值的连续存储合并到更宽的存储中) ... ... 更新: Gcc bug 82434-fstore-merge 不能可靠地工作。

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

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

如果一个 c + + 对象有一个非平凡的复制建构子或者一个 非平凡的析构函数,它通过不可见的引用传递。

整理 C + + 混乱的 optional是不容易的,但似乎有一个 至少在我检查的实现的 optional_base类中有非平凡的复制建构子

Libstdc + + 显然没有实现 P0602“变量和可选应该传播拷贝/移动平凡”:

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

它在 libstdc + + 中失败,并且通过了 libc + + 和 MSVC 标准库 (它确实需要一个合适的名称,所以我们不必称它为“ C++标准程式库的 MSVC 实现”或者“ MSVC STL”)。

当然 MSVC 还是不会在寄存器中传递 optional<int>,因为 MSABI。

编辑: 这个问题已经在 GCC 8发行版系列中得到解决。

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

执行此优化将不会对任何(定义良好的)程序的可观察行为产生影响,因此在 仿佛统治一切下是隐式允许的。然而,由于在其他答案中已经解释过的原因,编译器还没有明确地意识到这个事实,因此需要从头开始推断。行为静态分析是 本来就很难,因此编译器可能无法证明这种优化在所有情况下都是安全的。

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

std::is_trivially_copyable_v<std::optional<int>>应该是正确的。如果是这样,编译器就会更容易发现和执行这种优化。回答你的问题:

这是一个 QoI 问题,还是 std::optional规范中有什么东西阻止了这种优化?

两者都是。该规范使得优化非常难以找到,并且实现不够“聪明”,无法在这些约束条件下找到它。


假设你没做什么奇怪的事,比如 #define int something_else * 。