Als ik een variabele in een functie heb (bijvoorbeeld een grote array), heeft het dan zin om deze zowel static
als constexpr
te declareren? constexpr
garandeert dat de array tijdens het compileren wordt gemaakt, dus zou de static
nutteloos zijn?
void f() {
static constexpr int x [] = {
// a few thousand elements
};
// do something with the array
}
Doet de static
daar eigenlijk iets in termen van gegenereerde code of semantiek?
Antwoord 1, autoriteit 100%
Het korte antwoord is dat static
niet alleen nuttig is, maar ook altijd gewenst zal zijn.
Houd er eerst rekening mee dat static
en constexpr
volledig onafhankelijk van elkaar zijn. static
definieert de levensduur van het object tijdens de uitvoering; constexpr
specificeert 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 constexpr
niet langer relevant.
Elke variabele gedeclareerd constexpr
is impliciet const
maar const
en static
zijn bijna orthogonaal (behalve de interactie met static const
gehele 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 constexpr
gebruiken in je voorbeeld.
Er is echter één geval waarin u static constexpr
niet zou willen gebruiken. Tenzij een constexpr
gedeclareerd object ODR-gebruikt is of static
gedeclareerd, 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 static
willen gebruiken, aangezien static
het object waarschijnlijk dwingt om tijdens runtime te bestaan.
Antwoord 2, autoriteit 7%
Naast het gegeven antwoord is het vermeldenswaard dat de compiler de variabele constexpr
niet hoeft te initialiseren tijdens het compileren, wetende dat het verschil tussen constexpr
en static constexpr
is dat om static constexpr
te gebruiken, je ervoor zorgt dat de variabele slechts één keer wordt geïnitialiseerd.
De volgende code laat zien hoe de variabele constexpr
meerdere keren wordt geïnitialiseerd (met echter dezelfde waarde), terwijl static constexpr
zeker maar één keer wordt geïnitialiseerd.
Bovendien vergelijkt de code het voordeel van constexpr
met const
in 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 constexpr
meerdere keren geïnitialiseerd (adres is niet hetzelfde) terwijl het static
trefwoord ervoor zorgt dat initialisatie slechts één keer wordt uitgevoerd.
Antwoord 3, autoriteit 3%
Het niet maken van grote arrays static
, zelfs als ze constexpr
zijn, 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 -O3
bar()
compileert naar een jmp
naar 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 table
niet 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 constexpr
hier 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 table
static
hebben 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 constexpr
zijn. Clang optimaliseert dergelijke opzoektabellen eigenlijk goed, maar andere compilers niet. Zie Compiler Explorer voor een live voorbeeld.