Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the construction of std::optional<int> more expensive than a std::pair<int, bool>?

Consider these two approaches that can represent an "optional int":

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

Given these two functions...

auto get_std_optional_int() -> std_optional_int 
{
    return {42};
}

auto get_my_optional() -> my_optional_int 
{
    return {42, true};
}

...both g++ trunk and clang++ trunk (with -std=c++17 -Ofast -fno-exceptions -fno-rtti) produce the following assembly:

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


Why does get_std_optional_int() require three mov instructions, while get_my_optional() only needs a single movabs? Is this a QoI issue, or is there something in std::optional's specification preventing this optimization?

Also please note that users of the functions might be completely optimized out regardless:

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

int main()
{
    a = get_std_optional_int().value();
    b = get_my_optional().first;
}

...results in:

main:
        mov     DWORD PTR a[rip], 42
        xor     eax, eax
        mov     DWORD PTR b[rip], 42
        ret
like image 565
Vittorio Romeo Avatar asked Oct 03 '17 11:10

Vittorio Romeo


People also ask

What is std :: optional used for?

We use std::optional to make our code more expressive. std::optional contains the object within itself, depending on where it is stored (stack/data/heap) std::optional makes a copy of the contained object.

Is std optional efficient?

As a conclusion, std::optional is as efficient as a custom type to represent an optional integer value. Don't implement your own type, simply use the standard type.

Is std :: optional thread safe?

Optional is immutable, so it's automatically thread safe.

Does std optional use dynamic memory?

According to the standard std::optional is prohibited to use dynamic memory for their direct members.


4 Answers

libstdc++ apparently does not implement P0602 "variant and optional should propagate copy/move triviality". You can verify this with:

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

which fails for libstdc++, and passes for libc++ and the MSVC standard library (which really needs a proper name so we don't have to call it either "The MSVC implementation of the C++ standard library" or "The MSVC STL").

Of course MSVC still won't pass an optional<int> in a register because the MS ABI.

EDIT: This issue has been fixed in the GCC 8 release series.

like image 58
Casey Avatar answered Oct 17 '22 18:10

Casey


Why does get_std_optional_int() require three mov instructions, while get_my_optional() only needs a single movabs?

The direct cause is that optional is returned through a hidden pointer while pair is returned in a register. Why is that, though? The SysV ABI specification, section 3.2.3 Parameter Passing says:

If a C++ object has either a non-trivial copy constructor or a non-trivial destructor, it is passed by invisible reference.

Sorting out the C++ mess that is optional is not easy, but there seem to be a non-trivial copy constructor at least in the optional_base class of the implementation I checked.

like image 45
Jester Avatar answered Oct 17 '22 17:10

Jester


In Calling conventions for different C++ compilers and operating systems by Agner Fog it says that a copy constructor or destructor prevents from returning a structure in registers. This explains why optional is not returned in registers.

There has to be something else preventing the compiler from doing store merging (merges contiguous stores of immediate values narrower than a word into fewer wider stores to reduce the number of instructions)... Update: gcc bug 82434 - -fstore-merging does not work reliably.

like image 15
Maxim Egorushkin Avatar answered Oct 17 '22 18:10

Maxim Egorushkin


The optimization is technically permitted, even with std::is_trivially_copyable_v<std::optional<int>> being false. However, it may require an unreasonable degree of "cleverness" for the compiler to find. Also, for the specific case of using std::optional as the return type of a function, the optimization may need to be done at link-time rather than compile-time.

Performing this optimization would have no effect on any (well-defined) program's observable behavior,* and is therefore implicitly allowed under the as-if rule. However, for reasons which are explained in other answers, the compiler has not been explicitly made aware of that fact and would need to infer it from scratch. Behavioral static analysis is inherently difficult, so the compiler may not be able to prove that this optimization is safe under all circumstances.

Assuming the compiler can find this optimization, it would then need to alter this function's calling convention (i.e. change how the function returns a given value), which normally needs to be done at link time because the calling convention affects all of the call sites. Alternatively, the compiler could inline the function entirely, which may or may not be possible to do at compile time. These steps would not be necessary with a trivially-copyable object, so in this sense the standard does inhibit and complicate the optimization.

std::is_trivially_copyable_v<std::optional<int>> ought to be true. If it were true, it would be much easier for compilers to discover and perform this optimization. So, to answer your question:

Is this a QoI issue, or is there something in std::optional's specification preventing this optimization?

It's both. The spec makes the optimization substantially harder to find, and the implementation is not "smart" enough to find it under those constraints.


* Assuming you haven't done something really weird, like #define int something_else.

like image 3
Kevin Avatar answered Oct 17 '22 19:10

Kevin