Waarom is de constructie van std::optioneel<int> duurder dan een std::pair<int, bool>?

Overweeg deze twee benaderingen die een “optionele int” kunnen vertegenwoordigen:

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

Gezien deze twee functies…

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

…zowel g++ trunkals clang++ trunk(met -std=c++17 -Ofast -fno-exceptions -fno-rtti)produceer de volgende 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 voorbeeld op godbolt.org


Waarom vereist get_std_optional_int()drie movinstructies, terwijl get_my_optional()slechts één movabs?Is dit een QoI-probleem, of is er iets in de specificatie van std::optionaldat deze optimalisatie verhindert?

Houd er ook rekening mee dat gebruikers van de functies volledig kunnen worden geoptimaliseerd, ongeacht:

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

…resulteert in:

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

Antwoord 1, autoriteit 100%

libstdc++ implementeert blijkbaar geen P0602 “variant en optioneel zou de trivialiteit van kopiëren/verplaatsen moeten propageren”. U kunt dit verifiëren met:

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

die faalt voor libstdc++ en doorgaat voor libc++ en de MSVC-standaardbibliotheek(die echt een goede naam zodat we het niet “De MSVC-implementatie van de C++-standaardbibliotheek” of “De MSVC STL”) hoeven te noemen.

Natuurlijk zal MSVC noggeen optional<int>doorgeven in een register omdat de MS ABI.

EDIT: dit probleem is opgelost in de GCC 8-releaseserie.


Antwoord 2, autoriteit 43%

Waarom vereist get_std_optional_int()drie movinstructies, terwijl
get_my_optional()heeft maar één movabsnodig?

De directe oorzaak is dat optionalwordt geretourneerd via een verborgen aanwijzer terwijl pairwordt geretourneerd in een register. Waarom is dat echter? De SysV ABI-specificatie, sectie 3.2.3 Parameters doorgevenzegt:

Als een C++-object een niet-triviale kopie-constructor heeft of a
niet-triviale destructor, het wordt doorgegeven door onzichtbare referentie.

Het oplossen van de C++-rommel die optionalis, is niet eenvoudig, maar er lijkt een niet-triviale kopie-constructor in ieder geval in de klasse optional_basevan de implementatie die ik heb gecontroleerd.


Antwoord 3, autoriteit 34%

In Aanroepconventies voor verschillende C++-compilers en besturingssystemen van Agner Fogstaat dat een kopie-constructor of destructor verhindert dat een structuur in registers wordt geretourneerd. Dit verklaart waarom optionalniet wordt geretourneerd in registers.

Er moet iets anders zijn dat de compiler ervan weerhoudt om store merging uit te voeren (voegt aaneengesloten winkels met directe waarden die kleiner zijn dan een woord samen in minder bredere winkels om het aantal instructies te verminderen)… Update:gcc bug 82434 – -fstore-merging werkt niet betrouwbaar.


Antwoord 4, autoriteit 7%

De optimalisatie is technisch toegestaan, zelfs als std::is_trivially_copyable_v<std::optional<int>>false is. Het kan echter een onredelijke mate van “slimheid” vereisen voor de compiler om te vinden. Voor het specifieke geval van het gebruik van std::optionalals het retourtype van een functie, moet de optimalisatie mogelijk worden uitgevoerd tijdens de koppelingstijd in plaats van tijdens het compileren.

Het uitvoeren van deze optimalisatie zou geen effect hebben op het waarneembare gedrag van een (goed gedefinieerd) programma,* en is daarom impliciet toegestaan ​​onder de als -if regel. Om redenen die in andere antwoorden worden uitgelegd, is de compiler echter niet expliciet op de hoogte gebracht van dat feit en zou hij het vanaf het begin moeten afleiden. Gedragsstatische analyse is inherent moeilijk, dus de compiler kan mogelijk niet bewijzen dat dit optimalisatie is onder alle omstandigheden veilig.

Ervan uitgaande dat de compiler deze optimalisatie kan vinden, zou hij de aanroepconventie van deze functie moeten wijzigen (dwz veranderen hoe de functie een bepaalde waarde retourneert), wat normaal gesproken moet worden gedaan tijdens de koppeling, omdat de aanroepconventie van invloed is op alle bel sites. Als alternatief kan de compiler de functie volledig inline maken, wat al dan niet mogelijk is tijdens het compileren. Deze stappen zouden niet nodig zijn met een triviaal kopieerbaar object, dus in die zin remt en compliceert de standaard de optimalisatie.

std::is_trivially_copyable_v<std::optional<int>>zou waar moeten zijn. Als het waar zou zijn, zou het voor compilers veel gemakkelijker zijn om deze optimalisatie te ontdekken en uit te voeren. Dus, om je vraag te beantwoorden:

Is dit een QoI-probleem, of is er iets in de specificatie van std::optionaldat deze optimalisatie verhindert?

Het is allebei. De specificatie maakt de optimalisatie aanzienlijk moeilijker te vinden en de implementatie is niet “slim” genoeg om deze onder die beperkingen te vinden.


* Ervan uitgaande dat je niet iets heel raars hebt gedaan, zoals #define int something_else.

Other episodes