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) Version DoNotOptimize
aus der Google Benchmark-Bibliothek extrahiert, die Sie hier finden: https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208
#include <chrono>
template <class T>
__attribute__((always_inline)) inline void DoNotOptimize(const T &value) {
asm volatile("" : "+m"(const_cast<T &>(value)));
}
// The compiler has full knowledge of the implementation.
static int foo(int x) { return x * 2; }
auto time_foo() {
using Clock = std::chrono::high_resolution_clock;
auto input = 42;
auto t1 = Clock::now(); // Statement 1
DoNotOptimize(input);
auto output = foo(input); // Statement 2
DoNotOptimize(output);
auto t2 = Clock::now(); // Statement 3
return t2 - t1;
}
Hier 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:
% ./bin/clang++ -std=c++14 -c -S -o - so.cpp -O3
.text
.file "so.cpp"
.globl _Z8time_foov
.p2align 4, 0x90
.type _Z8time_foov,@function
_Z8time_foov: # @_Z8time_foov
.cfi_startproc
# BB#0: # %entry
pushq %rbx
.Ltmp0:
.cfi_def_cfa_offset 16
subq $16, %rsp
.Ltmp1:
.cfi_def_cfa_offset 32
.Ltmp2:
.cfi_offset %rbx, -16
movl $42, 8(%rsp)
callq _ZNSt6chrono3_V212system_clock3nowEv
movq %rax, %rbx
#APP
#NO_APP
movl 8(%rsp), %eax
addl %eax, %eax # This is "foo"!
movl %eax, 12(%rsp)
#APP
#NO_APP
callq _ZNSt6chrono3_V212system_clock3nowEv
subq %rbx, %rax
addq $16, %rsp
popq %rbx
retq
.Lfunc_end0:
.size _Z8time_foov, .Lfunc_end0-_Z8time_foov
.cfi_endproc
.ident "clang version 3.9.0 (trunk 273389) (llvm/trunk 273380)"
.section ".note.GNU-stack","",@progbits
Hier können Sie sehen, wie der Compiler den Aufruf auf foo(input)
einen einzelnen Befehl optimiert addl %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 .