Angenommen, ich habe eine Reihe von Anweisungen, die ich in einer festen Reihenfolge ausführen möchte. Ich möchte g ++ mit Optimierungsstufe 2 verwenden, damit einige Anweisungen neu angeordnet werden können. Welche Werkzeuge hat man, um eine bestimmte Reihenfolge von Anweisungen durchzusetzen?
Betrachten Sie das folgende Beispiel.
using Clock = std::chrono::high_resolution_clock;
auto t1 = Clock::now(); // Statement 1
foo(); // Statement 2
auto t2 = Clock::now(); // Statement 3
auto elapsedTime = t2 - t1;
In diesem Beispiel ist es wichtig, dass die Anweisungen 1-3 in der angegebenen Reihenfolge ausgeführt werden. Kann der Compiler jedoch nicht denken, dass Anweisung 2 unabhängig von 1 und 3 ist, und den Code wie folgt ausführen?
using Clock=std::chrono::high_resolution_clock;
foo(); // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3
auto elapsedTime = t2 - t1;
c++
c++11
operator-precedence
S2108887
quelle
quelle
__sync_synchronize()
hilfreich sein?foo
Ausführungszeit zu messen , die der Compiler bei der Neuordnung ignorieren darf, genauso wie er die Beobachtung von einem anderen Thread ignorieren darf.Antworten:
Ich möchte versuchen, eine etwas umfassendere Antwort zu geben, nachdem dies mit dem C ++ - Standardkomitee besprochen wurde. Ich bin nicht nur Mitglied des C ++ - Komitees, sondern auch Entwickler der LLVM- und Clang-Compiler.
Grundsätzlich gibt es keine Möglichkeit, eine Barriere oder eine Operation in der Sequenz zu verwenden, um diese Transformationen zu erreichen. Das grundlegende Problem besteht darin, dass die Betriebssemantik einer ganzzahligen Addition vollständig bekannt ist der Implementierung . Es kann sie simulieren, es weiß, dass sie von korrekten Programmen nicht beobachtet werden können, und es ist immer frei, sie zu bewegen.
Wir könnten versuchen, dies zu verhindern, aber es hätte äußerst negative Ergebnisse und würde letztendlich scheitern.
Die einzige Möglichkeit, dies im Compiler zu verhindern, besteht darin, ihm mitzuteilen, dass alle diese grundlegenden Operationen beobachtbar sind. Das Problem ist, dass dies dann die überwiegende Mehrheit der Compiler-Optimierungen ausschließen würde. Innerhalb des Compilers haben wir im Wesentlichen keine guten Mechanismen, um zu modellieren, dass das Timing beobachtbar ist, aber sonst nichts. Wir haben nicht einmal ein gutes Modell dafür, welche Operationen Zeit brauchen . Nimmt die Konvertierung einer vorzeichenlosen 32-Bit-Ganzzahl in eine vorzeichenlose 64-Bit-Ganzzahl beispielsweise Zeit in Anspruch? Auf x86-64 dauert es keine Zeit, auf anderen Architekturen dauert es jedoch nicht null. Hier gibt es keine allgemein korrekte Antwort.
Aber selbst wenn es uns durch einige Heldentaten gelingt, den Compiler daran zu hindern, diese Operationen neu zu ordnen, gibt es keine Garantie dafür, dass dies ausreicht. Überlegen Sie sich eine gültige und konforme Methode zum Ausführen Ihres C ++ - Programms auf einem x86-Computer: DynamoRIO. Dies ist ein System, das den Maschinencode des Programms dynamisch auswertet. Eine Sache, die es tun kann, sind Online-Optimierungen, und es ist sogar in der Lage, den gesamten Bereich grundlegender arithmetischer Anweisungen außerhalb des Timings spekulativ auszuführen. Und dieses Verhalten ist nicht nur bei dynamischen Evaluatoren zu beobachten. Die tatsächliche x86-CPU spekuliert auch (eine viel geringere Anzahl von) Anweisungen und ordnet sie dynamisch neu an.
Die wesentliche Erkenntnis ist, dass die Tatsache, dass Arithmetik nicht beobachtbar ist (selbst auf der Timing-Ebene), die Schichten des Computers durchdringt. Dies gilt für den Compiler, die Laufzeit und häufig sogar für die Hardware. Das Erzwingen der Beobachtbarkeit würde sowohl den Compiler als auch die Hardware dramatisch einschränken.
Aber all dies sollte nicht dazu führen, dass Sie die Hoffnung verlieren. Wenn Sie die Ausführung grundlegender mathematischer Operationen zeitlich festlegen möchten, haben wir gut untersuchte Techniken studiert, die zuverlässig funktionieren. Typischerweise werden diese beim Micro-Benchmarking verwendet . Ich habe auf der CppCon2015 einen Vortrag darüber gehalten: https://youtu.be/nXaxk27zwlk
Die dort gezeigten Techniken werden auch von verschiedenen Micro-Benchmark-Bibliotheken wie Googles bereitgestellt: https://github.com/google/benchmark#preventing-optimization
Der Schlüssel zu diesen Techniken besteht darin, sich auf die Daten zu konzentrieren. Sie machen die Eingabe in die Berechnung für den Optimierer undurchsichtig und das Ergebnis der Berechnung für den Optimierer undurchsichtig. Sobald Sie das getan haben, können Sie es zuverlässig zeitlich festlegen. Schauen wir uns eine realistische Version des Beispiels in der ursprünglichen Frage an, wobei die Definition für
foo
die Implementierung vollständig sichtbar ist. Ich habe auch eine (nicht portable) VersionDoNotOptimize
aus der Google Benchmark-Bibliothek extrahiert, die Sie hier finden: https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208Hier stellen wir sicher, dass die Eingabedaten und die Ausgabedaten um die Berechnung herum als nicht optimierbar markiert werden
foo
und nur um diese Markierungen herum die berechneten Timings. Da Sie Daten verwenden, um die Berechnung zu fixieren, bleibt diese garantiert zwischen den beiden Zeitpunkten, und dennoch kann die Berechnung selbst optimiert werden. Die resultierende x86-64-Assembly, die durch einen kürzlich erstellten Build von Clang / LLVM generiert wurde, lautet:Hier können Sie sehen, wie der Compiler den Aufruf auf
foo(input)
einen einzelnen Befehl optimiertaddl %eax, %eax
, ohne ihn jedoch außerhalb des Timings zu verschieben oder ihn trotz der konstanten Eingabe vollständig zu eliminieren.Ich hoffe, dies hilft, und das C ++ - Standardkomitee prüft die Möglichkeit, APIs ähnlich wie
DoNotOptimize
hier zu standardisieren .quelle
Clock::now()
relativ zu foo () neu angeordnet werden? Muss der Optimierer dies annehmenDoNotOptimize
undClock::now()
Zugriff auf einen gemeinsamen globalen Status haben und diesen ändern, der ihn wiederum an den Ein- und Ausgang binden würde? Oder verlassen Sie sich auf einige aktuelle Einschränkungen der Implementierung des Optimierers?DoNotOptimize
In diesem Beispiel handelt es sich um ein synthetisch "beobachtbares" Ereignis. Es ist, als würde eine sichtbare Ausgabe mit der Darstellung der Eingabe auf ein Terminal gedruckt. Da das Lesen der Uhr auch beobachtbar ist (Sie beobachten, wie die Zeit vergeht), können sie nicht neu angeordnet werden, ohne das beobachtbare Verhalten des Programms zu ändern.foo
Funktion einige Operationen wie das Lesen von einem Socket ausführt, der für eine Weile blockiert sein kann, zählt dies eine beobachtbare Operation? Und daread
es sich nicht um eine "völlig bekannte" Operation handelt (richtig?), Wird der Code in Ordnung bleiben?Zusammenfassung:
Es scheint keine garantierte Möglichkeit zu geben, eine Neuordnung zu verhindern. Solange jedoch die Optimierung der Verbindungszeit / des gesamten Programms nicht aktiviert ist, scheint es eine gute Wahl zu sein , die aufgerufene Funktion in einer separaten Kompilierungseinheit zu lokalisieren . (Zumindest bei GCC, obwohl die Logik vermuten lässt, dass dies auch bei anderen Compilern wahrscheinlich ist.) Dies geht zu Lasten des Funktionsaufrufs. Inline-Code befindet sich per Definition in derselben Kompilierungseinheit und kann neu angeordnet werden.
Ursprüngliche Antwort:
GCC ordnet die Aufrufe unter -O2-Optimierung neu an:
GCC 5.3.0:
g++ -S --std=c++11 -O0 fred.cpp
::Aber:
g++ -S --std=c++11 -O2 fred.cpp
::Nun mit foo () als externe Funktion:
g++ -S --std=c++11 -O2 fred.cpp
::ABER wenn dies mit -flto verknüpft ist (Link-Time-Optimierung):
quelle
Die Neuordnung kann vom Compiler oder vom Prozessor vorgenommen werden.
Die meisten Compiler bieten eine plattformspezifische Methode an, um eine Neuordnung von Lese- / Schreibanweisungen zu verhindern. Auf gcc ist dies
( Weitere Informationen hier )
Beachten Sie, dass dies nur indirekt Neuordnungsvorgänge verhindert, solange diese von den Lese- / Schreibvorgängen abhängen.
In der Praxis habe ich noch kein System gesehen, bei dem der Systemaufruf
Clock::now()
den gleichen Effekt hat wie eine solche Barriere. Sie können die resultierende Baugruppe überprüfen, um sicherzugehen.Es ist jedoch nicht ungewöhnlich, dass die zu testende Funktion während der Kompilierungszeit ausgewertet wird. Um eine "realistische" Ausführung zu erzwingen, müssen Sie möglicherweise Eingaben für
foo()
E / A oder einenvolatile
Lesevorgang ableiten .Eine andere Möglichkeit wäre, das Inlining für zu deaktivieren
foo()
- dies ist wiederum compilerspezifisch und normalerweise nicht portierbar, hätte aber den gleichen Effekt.Auf gcc wäre das
__attribute__ ((noinline))
@ Ruslan wirft ein grundlegendes Problem auf: Wie realistisch ist diese Messung?
Die Ausführungszeit wird von vielen Faktoren beeinflusst: Eine ist die tatsächliche Hardware, auf der wir ausgeführt werden, die andere ist der gleichzeitige Zugriff auf gemeinsam genutzte Ressourcen wie Cache-, Speicher-, Festplatten- und CPU-Kerne.
Was wir normalerweise tun, um vergleichbare Timings zu erhalten: Stellen Sie sicher, dass sie mit einer geringen Fehlerquote reproduzierbar sind . Das macht sie etwas künstlich.
Die Ausführungsleistung von "Hot Cache" und "Cold Cache" kann sich leicht um eine Größenordnung unterscheiden - aber in Wirklichkeit wird es etwas dazwischen sein ("lauwarm"?)
quelle
asm
beeinflusst die Ausführungszeit der Anweisungen zwischen Timer-Aufrufen: Der Code nach dem Speicher-Clobber muss alle Variablen aus dem Speicher neu laden.Die C ++ - Sprache definiert auf verschiedene Weise, was beobachtet werden kann.
Wenn
foo()
nichts beobachtbar ist, kann es vollständig beseitigt werden. Wennfoo()
nur eine Berechnung, die Werte im "lokalen" Zustand speichert (sei es auf dem Stapel oder irgendwo in einem Objekt) und der Compiler nachweisen kann, dass kein sicher abgeleiteter Zeiger in denClock::now()
Code gelangen kann, gibt es keine beobachtbaren Konsequenzen für Bewegen derClock::now()
Anrufe.Wenn
foo()
mit einer Feile oder dem Display interagiert, und der Compiler nicht nachweisen kann , dassClock::now()
tut nicht interact mit der Datei oder dem Display, dann Nachbestellung kann nicht getan werden, weil die Interaktion mit einer Datei oder einem Display beobachtbares Verhalten ist.Während Sie compilerspezifische Hacks verwenden können, um zu erzwingen, dass sich Code nicht bewegt (wie bei der Inline-Assembly), besteht ein anderer Ansatz darin, zu versuchen, Ihren Compiler zu überlisten.
Erstellen Sie eine dynamisch geladene Bibliothek. Laden Sie es vor dem betreffenden Code.
Diese Bibliothek enthüllt eines:
und verpackt es so:
Das packt ein nulläres Lambda und verwendet die dynamische Bibliothek, um es in einem Kontext auszuführen, den der Compiler nicht verstehen kann.
In der dynamischen Bibliothek machen wir:
das ist ziemlich einfach.
Um die Aufrufe an neu zu ordnen
execute
, muss es die dynamische Bibliothek verstehen, die es beim Kompilieren Ihres Testcodes nicht kann.Es kann immer noch
foo()
s ohne Nebenwirkungen eliminieren , aber Sie gewinnen einige, Sie verlieren einige.quelle
volatile
Zugriff zu verwenden oder externen Code aufzurufen.Nein, das kann es nicht. Gemäß dem C ++ - Standard [intro.execution]:
Ein vollständiger Ausdruck ist im Grunde eine Anweisung, die durch ein Semikolon abgeschlossen wird. Wie Sie sehen können, schreibt die obige Regel vor, dass Anweisungen in der richtigen Reihenfolge ausgeführt werden müssen. Es ist innerhalb von Aussagen , dass der Compiler mehr freien Lauf gelassen wird (dh es unter gewissen Umständen ist erlaubt Ausdrücke auszuwerten , die eine Aussage in Befehle anders als von links nach rechts oder irgendetwas anderes spezifische bilden).
Beachten Sie, dass die Bedingungen für die Anwendung der Als-ob-Regel hier nicht erfüllt sind. Es ist unangemessen zu glauben, dass jeder Compiler nachweisen kann , dass das Neuordnen von Aufrufen zum Abrufen der Systemzeit das beobachtbare Programmverhalten nicht beeinflusst. Wenn es einen Umstand gäbe, unter dem zwei Aufrufe zum Abrufen der Zeit neu angeordnet werden könnten, ohne das beobachtete Verhalten zu ändern, wäre es äußerst ineffizient, tatsächlich einen Compiler zu erstellen, der ein Programm mit ausreichendem Verständnis analysiert, um dies mit Sicherheit ableiten zu können.
quelle
Nein.
Manchmal können Anweisungen nach der "Als-ob" -Regel neu angeordnet werden. Dies liegt nicht daran, dass sie logisch unabhängig voneinander sind, sondern daran, dass diese Unabhängigkeit eine solche Neuordnung ermöglicht, ohne die Semantik des Programms zu ändern.
Das Verschieben eines Systemaufrufs, der die aktuelle Zeit erhält, erfüllt diese Bedingung offensichtlich nicht. Ein Compiler, der dies wissentlich oder unwissentlich tut, ist nicht konform und wirklich albern.
Im Allgemeinen würde ich nicht erwarten, dass ein Ausdruck, der zu einem Systemaufruf führt, selbst von einem aggressiv optimierenden Compiler "hinterfragt" wird. Es weiß einfach nicht genug darüber, was dieser Systemaufruf bewirkt.
quelle
int x = 0; clock(); x = y*2; clock();
gibt es keine definierten Möglichkeiten für dieclock()
Interaktion des Codes mit dem Status vonx
. Nach dem C ++ - Standard muss es nicht wissen, was esclock()
tut - es könnte den Stapel untersuchen (und feststellen, wann die Berechnung erfolgt), aber das ist nicht das Problem von C ++ .t2
und des zweiten zugewiesen wird,t1
nicht konform und albern wäre, wenn diese Werte verwendet werden. Was diese Antwort vermisst, ist das Ein konformer Compiler kann manchmal anderen Code während eines Systemaufrufs neu anordnen. In diesem Fall kann es, sofern es weiß, was esfoo()
tut (zum Beispiel, weil es es eingefügt hat) und daher (lose gesagt) eine reine Funktion ist, es bewegen.y*y
vor dem Systemaufruf nicht spekulativ berechnet wird, nur zum Spaß. Es gibt auch keine Garantie dafür, dass die tatsächliche Implementierung das Ergebnis dieser spekulativen Berechnung später an keinem beliebigen Punktx
verwendet und daher zwischen den Aufrufen von nichts unternimmtclock()
. Das Gleiche gilt für alles, was eine Inline-Funktionfoo
tut, vorausgesetzt, sie hat keine Nebenwirkungen und kann nicht von dem Zustand abhängen, durch den sie geändert werden könnteclock()
.noinline
Funktion + Blackbox für Inline-Assembly + vollständige DatenabhängigkeitenDies basiert auf https://stackoverflow.com/a/38025837/895245, aber weil ich keine klare Rechtfertigung dafür gesehen habe, warum die
::now()
nicht nachbestellt werden kann, wäre ich lieber paranoid und würde es zusammen mit dem in eine Noinline-Funktion einfügen asm.Auf diese Weise bin ich mir ziemlich sicher, dass die Neuordnung nicht stattfinden kann, da die
noinline
"Bindungen" die::now
und die Datenabhängigkeit .main.cpp
GitHub stromaufwärts .
Kompilieren und ausführen:
Der einzige kleine Nachteil dieser Methode ist, dass wir
callq
einerinline
Methode eine zusätzliche Anweisung hinzufügen .objdump -CD
zeigt, dassmain
enthält:so sehen wir, dass
foo
das inline war, aberget_clock
nicht war und es umgibt.get_clock
selbst ist jedoch äußerst effizient und besteht aus einer für einen einzelnen Blattaufruf optimierten Anweisung, die nicht einmal den Stapel berührt:Da die Taktgenauigkeit selbst begrenzt ist, halte ich es für unwahrscheinlich, dass Sie die Timing-Effekte eines zusätzlichen Objekts bemerken
jmpq
. Beachten Sie, dass einecall
unabhängig davon erforderlich ist, da sie::now()
sich in einer gemeinsam genutzten Bibliothek befindet.Aufruf
::now()
von einer Inline-Assembly mit einer DatenabhängigkeitDies wäre die effizienteste Lösung, die möglich wäre, und sogar das
jmpq
oben erwähnte Extra zu überwinden .Dies ist leider äußerst schwierig, wie unter: printf in erweitertem Inline-ASM aufrufen
Wenn Ihre Zeitmessung jedoch direkt in der Inline-Montage ohne Anruf durchgeführt werden kann, kann diese Technik verwendet werden. Dies ist beispielsweise bei gem5 magischen Instrumentierungsanweisungen , x86 RDTSC (nicht sicher, ob dies nicht mehr repräsentativ ist) und möglicherweise anderen Leistungsindikatoren der Fall .
Verwandte Themen:
Getestet mit GCC 8.3.0, Ubuntu 19.04.
quelle
"+m"
, indem"+r"
eine viel effizientere Art und Weise der Compiler einen Wert materialisieren zu machen und dann übernehmen die Variablen verändert haben.