为什么构造std :: optional <int>比std :: pair <int,bool>昂贵?


69

考虑以下两种可以表示“可选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 ++干线clang ++干线 (带有-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

6
optional通过隐藏指针返回,这意味着类型定义包含禁止通过寄存器返回的内容。
小丑2017年

1
明显的区别在于,这std::pair是一个合计,而std::optional不是。不知道它是否应该起作用,但是您知道...
StoryTeller-Unslander Monica

3
boost::optional在任何版本的GCC上,同样的问题,演示都不
John Zwinck

3
聚合类型与非聚合类型,SYS V x64 ABI以及4294967338为0x10000002a的事实应清楚说明这一点。
玛格丽特·布鲁姆

3
@WojciechMigdafolly::Optional没有必要的魔法来使其特殊的成员函数有条件地变得微不足道。(它也通过使用内None联函数中的内部链接来违反ODR ,并且每个单个constexprFOLLY_CPP14_CONSTEXPR函数都是格式不正确的NDR:您不能使用来实现optionalconstexprAPI aligned_storage。)+1是可co_await实现的,但效果会更好optional从range-v3窃取实现并添加其他API。
Casey

Answers:


43

libstdc ++显然未实现P0602“变量和可选变量应传播复制/移动琐碎性”。您可以使用以下方法进行验证:

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

它对于libstdc ++失败,并通过libc ++和MSVC标准库(它确实需要一个专有名称,因此我们不必将其称为“ C ++标准库的MSVC实现”或“ MSVC STL”)。

当然,由于MS ABI,MSVC仍然不会通过optional<int>寄存器。

编辑:GCC 8版本系列中已解决此问题。


optional尽管仍然需要一个析构函数,并且阻止了将其返回到寄存器中。
Maxim Egorushkin

6
@MaximEgorushkin它仅在T具有一个析构函数时才需要。
棘轮怪胎

1
那份文件被通过了吗?最新的草案中没有反映出来。
巴里

1
@Barry Per这是它现在位于Library Evolution中。
NathanOliver

1
正确,尚未采用P0602。笔者做出了尝试与主要实施者,使沟通optionalvector可以“固定”人船和锁ABI之前。我相信libstdc ++ / libc ++ / MSFTvariant都可以遵循,就像libc ++ / MSFT一样optional,但是显然libstdc ++optional维护者没有得到备忘录。
Casey

17

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

直接原因是在寄存器中optional返回时通过隐藏指针pair返回的。为什么会这样呢?SysV ABI规范的3.2.3节“参数传递”中指出:

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

整理出C ++的混乱optional并不是一件容易的事,但是至少在optional_base我检查过的实现类中,似乎有一个简单的复制构造函数


3
不一定是,但可以是,这对于编译器来说很重要。因此,您实际上必须查看实现。
小丑2017年

15

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

必须采取其他措施阻止编译器进行存储合并(将比一个字窄的立即值的连续存储合并到较少的较宽存储中,以减少指令数量)...更新: gcc错误82434--fstore-merging不工作可靠。


知道这个“其他”可能是什么吗?
Daniel H

x86-64没有,mov mem, imm64因此您无法合并商店。
小丑2017年

@Jester可以加载rax合并的值并将其存储到中[rdi]
Maxim Egorushkin

当然可以,但是我认为这没有固有的速度优势,这是较短的机器代码。它当然不会减少指令的数量。
小丑2017年

1
@Maxim:gcc7实现了存储合并(gcc3或gcc4中断了,因此gcc多年来一直被相邻的狭窄分配所吸引),但是在填充对象方面仍然缺少优化。 gcc.gnu.org/bugzilla/show_bug.cgi?id=82142。(在这种情况下,clang似乎缺少相同的优化。)如果您编写了一个通过pair<int,int>指针存储的函数,则gcc和clang将合并存储:godbolt.org/g/44zodQ
彼得·科德斯

3

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

执行此优化不会对任何(定义明确的)程序的可观察到的行为*产生任何影响,因此在as-if规则下是隐式允许的。但是,由于其他答案中已说明的原因,编译器尚未明确意识到该事实,因此需要从头开始进行推断。行为静态分析本质上困难的,因此编译器可能无法证明此优化在所有情况下都是安全的。

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

std::is_trivially_copyable_v<std::optional<int>>应该是真的。如果为真,则编译器发现并执行此优化将容易得多。因此,回答您的问题:

这是QoI问题,还是在std::optional规范中有阻止此优化的内容?

都是。该规范使优化工作几乎很难找到,并且该实现还不够“聪明”,无法在这些限制下找到优化过程。


*假设您没有做过真正奇怪的事情,例如#define int something_else


1
您将调用约定作为“实现”的一部分包括在内。从技术上讲,这是正确的,但是除非您启用整个程序/链接时间优化,否则给定平台上的编译器甚至不会尝试通过完全内联或制作恒定传播克隆函数版本来尝试进行更改。
彼得·科德斯

我猜gcc可能会发出类似.gcc_reg_return_get_std_optional_int.clone123普通ABI定义的内容。但是,来自其他翻译部门的调用者不能认为这是存在的,因此,他们必须调用常规版本(除非您使用LTO,在这种情况下,它会内联,因为它很小)。但是,如果函数实际上很大,那么确保克隆该函数的备用呼叫约定版本将很有用。可能最有用的是在2个单独的reg中返回零件,而不是包装到RAX中。
彼得·科德斯

@PeterCordes:你确定吗?在这里,该功能的实现似乎完全无关紧要,仅需要实现,std::optional并且由于它是模板,因此其实现始终可用。
Matthieu M.

@MatthieuM .:是的,我有信心gcc和clang遵循他们决定使用的C ++ ABI,如果返回或按值传递时足够小,则打包到寄存器std::is_trivially_copyable_v<foo>的标准为。您可能会更改C ++ ABI,以针对存在析构函数但已知能够进行优化的模板类实现更复杂的规则。但这可能需要始终启用优化功能,以使编译器在如何传递某些对象方面彼此达成共识。(例如,内联并优化。)
彼得·科德斯

C ++ ABI并非旨在稳定C ABI /调用约定(x86-64 System V psABI)的方式,但是不同的编译器必须可靠地彼此一致。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.