Is de statische constexpr-variabele in een functie logisch?

Als ik een variabele in een functie heb (bijvoorbeeld een grote array), heeft het dan zin om deze zowel staticals constexprte declareren? constexprgarandeert dat de array tijdens het compileren wordt gemaakt, dus zou de staticnutteloos zijn?

void f() {
    static constexpr int x [] = {
        // a few thousand elements
    };
    // do something with the array
}

Doet de staticdaar eigenlijk iets in termen van gegenereerde code of semantiek?


Antwoord 1, autoriteit 100%

Het korte antwoord is dat staticniet alleen nuttig is, maar ook altijd gewenst zal zijn.

Houd er eerst rekening mee dat staticen constexprvolledig onafhankelijk van elkaar zijn. staticdefinieert de levensduur van het object tijdens de uitvoering; constexprspecificeert dat het object beschikbaar moet zijn tijdens compilatie. Compilatie en uitvoering zijn onsamenhangend en niet aaneengesloten, zowel in tijd als in ruimte. Dus als het programma eenmaal is gecompileerd, is constexprniet langer relevant.

Elke variabele gedeclareerd constexpris impliciet constmaar consten staticzijn bijna orthogonaal (behalve de interactie met static constgehele getallen.)

Het C++objectmodel (§1.9) vereist dat alle objecten behalve bit-velden ten minste één byte geheugen innemen en adressen hebben; verder moeten al deze objecten die op een bepaald moment in een programma waarneembaar zijn, verschillende adressen hebben (paragraaf 6). Dit vereist niet echt dat de compiler een nieuwe array op de stapel maakt voor elke aanroep van een functie met een lokale niet-statische const-array, omdat de compiler zijn toevlucht zou kunnen nemen tot het gegeven as-if-principe het kan bewijzen dat geen ander dergelijk object kan worden waargenomen.

Dat zal helaas niet gemakkelijk te bewijzen zijn, tenzij de functie triviaal is (het roept bijvoorbeeld geen andere functie aan waarvan het lichaam niet zichtbaar is in de vertaaleenheid), omdat arrays min of meer per definitie zijn adressen. Dus in de meeste gevallen moet de niet-statische const(expr)-array bij elke aanroep opnieuw op de stapel worden gemaakt, wat het punt tenietdoet om het tijdens het compileren te kunnen berekenen.

Aan de andere kant wordt een lokaal static const-object gedeeld door alle waarnemers en kan het bovendien worden geïnitialiseerd, zelfs als de functie waarin het is gedefinieerd, nooit wordt aangeroepen. Dus geen van het bovenstaande is van toepassing, en een compiler is vrij om niet alleen een enkele instantie ervan te genereren; het is gratis om er één exemplaar van te genereren in alleen-lezen opslag.

Dus je moet zeker static constexprgebruiken in je voorbeeld.

Er is echter één geval waarin u static constexprniet zou willen gebruiken. Tenzij een constexprgedeclareerd object ODR-gebruikt is of staticgedeclareerd, staat het de compiler vrij om het helemaal niet op te nemen. Dat is best handig, omdat het het gebruik van tijdelijke constexpr-arrays tijdens het compileren toestaat zonder het gecompileerde programma te vervuilen met onnodige bytes. In dat geval zou je duidelijk geen staticwillen gebruiken, aangezien statichet object waarschijnlijk dwingt om tijdens runtime te bestaan.


Antwoord 2, autoriteit 7%

Naast het gegeven antwoord is het vermeldenswaard dat de compiler de variabele constexprniet hoeft te initialiseren tijdens het compileren, wetende dat het verschil tussen constexpren static constexpris dat om static constexprte gebruiken, je ervoor zorgt dat de variabele slechts één keer wordt geïnitialiseerd.

De volgende code laat zien hoe de variabele constexprmeerdere keren wordt geïnitialiseerd (met echter dezelfde waarde), terwijl static constexprzeker maar één keer wordt geïnitialiseerd.

Bovendien vergelijkt de code het voordeel van constexprmet constin combinatie met static.

#include <iostream>
#include <string>
#include <cassert>
#include <sstream>
const short const_short = 0;
constexpr short constexpr_short = 0;
// print only last 3 address value numbers
const short addr_offset = 3;
// This function will print name, value and address for given parameter
void print_properties(std::string ref_name, const short* param, short offset)
{
    // determine initial size of strings
    std::string title = "value \\ address of ";
    const size_t ref_size = ref_name.size();
    const size_t title_size = title.size();
    assert(title_size > ref_size);
    // create title (resize)
    title.append(ref_name);
    title.append(" is ");
    title.append(title_size - ref_size, ' ');
    // extract last 'offset' values from address
    std::stringstream addr;
    addr << param;
    const std::string addr_str = addr.str();
    const size_t addr_size = addr_str.size();
    assert(addr_size - offset > 0);
    // print title / ref value / address at offset
    std::cout << title << *param << " " << addr_str.substr(addr_size - offset) << std::endl;
}
// here we test initialization of const variable (runtime)
void const_value(const short counter)
{
    static short temp = const_short;
    const short const_var = ++temp;
    print_properties("const", &const_var, addr_offset);
    if (counter)
        const_value(counter - 1);
}
// here we test initialization of static variable (runtime)
void static_value(const short counter)
{
    static short temp = const_short;
    static short static_var = ++temp;
    print_properties("static", &static_var, addr_offset);
    if (counter)
        static_value(counter - 1);
}
// here we test initialization of static const variable (runtime)
void static_const_value(const short counter)
{
    static short temp = const_short;
    static const short static_var = ++temp;
    print_properties("static const", &static_var, addr_offset);
    if (counter)
        static_const_value(counter - 1);
}
// here we test initialization of constexpr variable (compile time)
void constexpr_value(const short counter)
{
    constexpr short constexpr_var = constexpr_short;
    print_properties("constexpr", &constexpr_var, addr_offset);
    if (counter)
        constexpr_value(counter - 1);
}
// here we test initialization of static constexpr variable (compile time)
void static_constexpr_value(const short counter)
{
    static constexpr short static_constexpr_var = constexpr_short;
    print_properties("static constexpr", &static_constexpr_var, addr_offset);
    if (counter)
        static_constexpr_value(counter - 1);
}
// final test call this method from main()
void test_static_const()
{
    constexpr short counter = 2;
    const_value(counter);
    std::cout << std::endl;
    static_value(counter);
    std::cout << std::endl;
    static_const_value(counter);
    std::cout << std::endl;
    constexpr_value(counter);
    std::cout << std::endl;
    static_constexpr_value(counter);
    std::cout << std::endl;
}

Mogelijke programma-uitvoer:

value \ address of const is               1 564
value \ address of const is               2 3D4
value \ address of const is               3 244
value \ address of static is              1 C58
value \ address of static is              1 C58
value \ address of static is              1 C58
value \ address of static const is        1 C64
value \ address of static const is        1 C64
value \ address of static const is        1 C64
value \ address of constexpr is           0 564
value \ address of constexpr is           0 3D4
value \ address of constexpr is           0 244
value \ address of static constexpr is    0 EA0
value \ address of static constexpr is    0 EA0
value \ address of static constexpr is    0 EA0

Zoals u zelf kunt zien, wordt constexprmeerdere keren geïnitialiseerd (adres is niet hetzelfde) terwijl het statictrefwoord ervoor zorgt dat initialisatie slechts één keer wordt uitgevoerd.


Antwoord 3, autoriteit 3%

Het niet maken van grote arrays static, zelfs als ze constexprzijn, kan een dramatische impact hebben op de prestaties en kan leiden tot veel gemiste optimalisaties. Het kan uw code met orden van grootte vertragen. Uw variabelen zijn nog steeds lokaal en de compiler kan besluiten om ze tijdens runtime te initialiseren in plaats van ze op te slaan als gegevens in het uitvoerbare bestand.

Beschouw het volgende voorbeeld:

template <int N>
void foo();
void bar(int n)
{
    // array of four function pointers to void(void)
    constexpr void(*table[])(void) {
        &foo<0>,
        &foo<1>,
        &foo<2>,
        &foo<3>
    };
    // look up function pointer and call it
    table[n]();
}

Je verwacht waarschijnlijk dat gcc-10 -O3bar()compileert naar een jmpnaar een adres dat het uit een tabel haalt, maar dat is niet wat er gebeurt:

bar(int):
        mov     eax, OFFSET FLAT:_Z3fooILi0EEvv
        movsx   rdi, edi
        movq    xmm0, rax
        mov     eax, OFFSET FLAT:_Z3fooILi2EEvv
        movhps  xmm0, QWORD PTR .LC0[rip]
        movaps  XMMWORD PTR [rsp-40], xmm0
        movq    xmm0, rax
        movhps  xmm0, QWORD PTR .LC1[rip]
        movaps  XMMWORD PTR [rsp-24], xmm0
        jmp     [QWORD PTR [rsp-40+rdi*8]]
.LC0:
        .quad   void foo<1>()
.LC1:
        .quad   void foo<3>()

Dit komt omdat GCC besluit tableniet op te slaan in de gegevenssectie van het uitvoerbare bestand, maar in plaats daarvan een lokale variabele met zijn inhoud initialiseert telkens wanneer de functie wordt uitgevoerd. Als we constexprhier verwijderen, is het gecompileerde binaire bestand zelfs 100% identiek.

Dit kan gemakkelijk 10x langzamer zijn dan de volgende code:

template <int N>
void foo();
void bar(int n)
{
    static constexpr void(*table[])(void) {
        &foo<0>,
        &foo<1>,
        &foo<2>,
        &foo<3>
    };
    table[n]();
}

Onze enige verandering is dat we tablestatichebben gemaakt, maar de impact is enorm:

bar(int):
        movsx   rdi, edi
        jmp     [QWORD PTR bar(int)::table[0+rdi*8]]
bar(int)::table:
        .quad   void foo<0>()
        .quad   void foo<1>()
        .quad   void foo<2>()
        .quad   void foo<3>()

Concluderend, maak nooit lokale variabelen van uw opzoektabellen, zelfs niet als het constexprzijn. Clang optimaliseert dergelijke opzoektabellen eigenlijk goed, maar andere compilers niet. Zie Compiler Explorer voor een live voorbeeld.

Other episodes