it-swarm.com.de

Ist der Zugriff auf eine statische Funktionsvariable langsamer als der Zugriff auf eine globale Variable?

Statische lokale Variablen werden beim ersten Funktionsaufruf initialisiert:

Variablen, die im Gültigkeitsbereich des Blocks mit dem Bezeichner static deklariert werden, haben eine statische Speicherdauer, werden jedoch initialisiert, wenn die Steuerung zum ersten Mal ihre Deklaration durchläuft (es sei denn, ihre Initialisierung ist eine Null- oder Konstantinitialisierung, die vor dem ersten Eintreten des Blocks durchgeführt werden kann). Bei allen weiteren Aufrufen wird die Deklaration übersprungen.

In C++ 11 gibt es noch mehr Prüfungen:

Wenn mehrere Threads versuchen, dieselbe statische lokale Variable gleichzeitig zu initialisieren, erfolgt die Initialisierung genau einmal (ein ähnliches Verhalten kann für beliebige Funktionen mit std :: call_once erhalten werden) . Hinweis: Übliche Implementierungen dieses Features verwenden Varianten des doppelt geprüften Sperrmusters, wodurch der Laufzeitaufwand für bereits initialisierte lokale Statik auf einen einzigen nicht-atomaren booleschen Vergleich reduziert wird. (seit C++ 11)

Gleichzeitig scheinen globale Variablen beim Programmstart initialisiert zu werden (obwohl technisch nur Allocation/deallocation in cppreference erwähnt wird):

statische Speicherdauer. Der Speicher für das Objekt wird zugewiesen, wenn das Programm beginnt, und es wird freigegeben, wenn das Programm endet. Es gibt nur eine Instanz des Objekts. Alle im Namespace-Bereich deklarierten Objekte (einschließlich des globalen Namespaces) haben diese Speicherdauer sowie die mit statisch oder extern deklarierten Objekte.

So gegeben das folgende Beispiel:

struct A {
    // complex type...
};
const A& f()
{
    static A local{};
    return local;
}

A global{};
const A& g()
{
    return global;
}

kann ich davon ausgehen, dass f() bei jedem Aufruf überprüfen muss, ob die Variable initialisiert wurde und f() langsamer ist als g()?

25
Dev Null

Sie sind natürlich konzeptionell korrekt, aber zeitgenössische Architekturen können damit umgehen.

Ein moderner Compiler und eine Architektur würden die Pipeline so anordnen, dass der bereits initialisierte Zweig angenommen wurde. Der Aufwand für die Initialisierung würde daher einen zusätzlichen Pipeline-Dump verursachen, das ist alles.

Wenn Sie Zweifel haben, überprüfen Sie die Baugruppe.

15
Bathsheba

Ja, es ist mit ziemlicher Sicherheit etwas langsamer. Die meiste Zeit spielt es jedoch keine Rolle und die Kosten werden durch den "Logik- und Stil" -Nutzen übergewichtet.

Technisch ist eine funktionslokale statische Variable dieselbe wie eine globale Variable. Nur, dass sein Name nicht global bekannt ist (was eine gute Sache ist) und seine Initialisierung garantiert nicht nur zu einem genau festgelegten Zeitpunkt, sondern auch nur einmal und threadsicher erfolgt.

Dies bedeutet, dass eine funktionslokale statische Variable muss weiß, ob eine Initialisierung stattgefunden hat, und daher mindestens einen zusätzlichen Speicherzugriff und einen bedingten Sprung benötigt, den die globale Variable (im Prinzip) nicht benötigt. Eine Implementierung kann etwas Ähnliches für Globale tun, aber es muss nicht (und normalerweise nicht).

Die Chancen stehen gut, dass der Sprung in allen Fällen bis auf zwei richtig vorhergesagt wird. Die ersten beiden Aufrufe werden höchstwahrscheinlich als falsch vorausgesagt (normalerweise wird angenommen, dass Sprünge beim ersten Aufruf nicht ausgeführt werden, und bei nachfolgenden Sprüngen wird davon ausgegangen, dass sie denselben Pfad wie der letzte verwenden, was wiederum falsch ist). Danach sollten Sie in der Lage sein, mit nahezu 100% korrekter Vorhersage loszulegen.
Aber auch ein korrekt vorhergesagter Sprung ist nicht frei (die CPU kann immer noch nur eine bestimmte Anzahl von Befehlen in jedem Zyklus starten, selbst wenn die Ausführung keine Zeit in Anspruch nimmt), aber es ist nicht viel. Wenn die Speicherlatenz, die im schlimmsten Fall einige hundert Zyklen betragen kann, erfolgreich ausgeblendet werden kann, verschwinden die Kosten fast im Pipelining. Außerdem ruft jeder Zugriff eine zusätzliche Cache-Zeile ab, die sonst nicht benötigt würde (das Flag wurde initialisiert ist wahrscheinlich nicht in derselben Cache-Zeile wie die Daten gespeichert). Sie haben also eine etwas schlechtere L1-Leistung (L2 sollte groß genug sein, damit Sie sagen können "Ja, na und").

Es muss auch tatsächlich etwas tun einmal und threadsicher, was das Globale (im Prinzip) nicht tun muss, zumindest nicht in einer Weise, die Sie sehen. Eine Implementierung kann etwas anderes tun, aber die meisten initialisieren nur Globale, bevor main eingegeben wird, und nicht selten geschieht dies mit einem memset oder implizit, weil die Variable in einem Segment gespeichert ist, das ist trotzdem genullt.
Ihre statische Variable muss muss initialisiert werden, wenn der Initialisierungscode ausgeführt wird, und dies muss threadsicher erfolgen. Abhängig davon, wie viel Ihre Implementierung saugt, kann dies ziemlich teuer sein. Ich habe mich entschieden, auf die Thread-Sicherheitsfunktion zu verzichten und immer mit fno-threadsafe-statics Zu kompilieren (auch wenn dies nicht standardkonform ist), nachdem ich festgestellt habe, dass GCC (ansonsten ein OK-Allround-Compiler) tatsächlich für jeden einen Mutex sperren würde statische Initialisierung.

6
Damon

Von https://de.cppreference.com/w/cpp/language/initialization

Verzögerte dynamische Initialisierung
Es ist implementierungsdefiniert, ob die dynamische Initialisierung vor der ersten Anweisung der Hauptfunktion (für Statik) oder der Anfangsfunktion des Threads (für Thread-Locals) erfolgt oder später verschoben wird.

Wenn die Initialisierung einer Nicht-Inline-Variablen (seit C++ 17) nach der ersten Anweisung der Haupt-/Thread-Funktion verzögert wird, geschieht dies vor der ersten odr-Verwendung einer Variablen mit statischer/Thread-Speicherdauer, die in der definiert ist gleiche Übersetzungseinheit wie die zu initialisierende Variable. 

Eine ähnliche Prüfung may muss daher auch für globale Variablen durchgeführt werden.

daher ist f() nicht erforderlich "langsamer" als g().

2
Jarod42

g() ist nicht threadsicher und für alle Arten von Bestellproblemen anfällig. Sicherheit wird zu einem Preis kommen. Es gibt mehrere Möglichkeiten, um es zu bezahlen:

f(), das Meyer's Singleton, zahlt den Preis für jeden Zugriff. Wenn Sie häufig auf diesen Code zugreifen oder in einem Abschnitt mit Verhaltensempfindlichkeit auf Ihren Code zugreifen, ist es sinnvoll, f() zu vermeiden. Ihr Prozessor verfügt vermutlich über eine begrenzte Anzahl von Schaltungen, die er der Verzweigungsvorhersage widmen kann, und Sie müssen sowieso vor dem Zweig eine atomare Variable lesen. Es ist ein großer Preis, ständig dafür zu bezahlen, dass die Initialisierung nur einmal erfolgt ist.

h(), wie unten beschrieben, arbeitet sehr ähnlich wie g() mit einer zusätzlichen Indirektion, geht jedoch davon aus, dass h_init() zu Beginn der Ausführung genau einmal aufgerufen wird. Vorzugsweise definieren Sie eine Subroutine, die als Zeile von main() aufgerufen wird. das ruft jede Funktion wie h_init() mit einer absoluten Reihenfolge auf. Hoffentlich müssen diese Objekte nicht zerstört werden.

Wenn Sie GCC verwenden, können Sie alternativ h_init() mit __attribute__((constructor)) kommentieren. Ich bevorzuge jedoch das explizite der statischen init-Routine.

A * h_global = nullptr;
void h_init() { h_global = new A { }; }
A const& h() { return *h_global; }

h2() ist genauso wie h(), ohne die zusätzliche Indirektion:

alignas(alignof(A)) char h2_global [sizeof(A)] = { };
void h2_init() { new (std::begin(h2_global)) A { }; }
A const& h2() { return * reinterpret_cast <A const *> (std::cbegin(h2_global)); }
0
KevinZ