Vorrei provare a fornire una risposta un po 'più completa dopo che questo è stato discusso con il comitato per gli standard C ++. Oltre ad essere un membro del comitato C ++, sono anche uno sviluppatore dei compilatori LLVM e Clang.
Fondamentalmente, non c'è modo di utilizzare una barriera o qualche operazione nella sequenza per ottenere queste trasformazioni. Il problema fondamentale è che la semantica operazionale di qualcosa come un'addizione intera è totalmente nota all'implementazione. Può simularli, sa che non possono essere osservati da programmi corretti ed è sempre libero di spostarli.
Potremmo tentare di impedirlo, ma avrebbe risultati estremamente negativi e alla fine fallirebbe.
Primo, l'unico modo per impedire che ciò accada nel compilatore è dirgli che tutte queste operazioni di base sono osservabili. Il problema è che questo quindi precluderebbe la stragrande maggioranza delle ottimizzazioni del compilatore. All'interno del compilatore, non abbiamo essenzialmente buoni meccanismi per modellare che la tempistica sia osservabile ma nient'altro. Non abbiamo nemmeno un buon modello di quali operazioni richiedono tempo . Ad esempio, la conversione di un intero senza segno a 32 bit in un intero senza segno a 64 bit richiede tempo? Non richiede tempo su x86-64, ma su altre architetture richiede tempo diverso da zero. Non c'è una risposta genericamente corretta qui.
Ma anche se riuscissimo con qualche eroismo a impedire al compilatore di riordinare queste operazioni, non c'è alcuna garanzia che ciò sia sufficiente. Considera un modo valido e conforme per eseguire il tuo programma C ++ su una macchina x86: DynamoRIO. Questo è un sistema che valuta dinamicamente il codice macchina del programma. Una cosa che può fare sono le ottimizzazioni online ed è persino in grado di eseguire speculativamente l'intera gamma di istruzioni aritmetiche di base al di fuori dei tempi. E questo comportamento non è esclusivo dei valutatori dinamici, la CPU x86 effettiva speculerà anche (un numero molto inferiore di) istruzioni e le riordinerà dinamicamente.
La consapevolezza essenziale è che il fatto che l'aritmetica non sia osservabile (anche a livello di tempo) è qualcosa che permea gli strati del computer. È vero per il compilatore, il runtime e spesso anche l'hardware. Costringerlo ad essere osservabile limiterebbe drasticamente il compilatore, ma vincolerebbe anche drasticamente l'hardware.
Ma tutto questo non dovrebbe farti perdere la speranza. Quando si desidera cronometrare l'esecuzione di operazioni matematiche di base, abbiamo tecniche ben studiate che funzionano in modo affidabile. In genere vengono utilizzati durante il micro-benchmarking . Ne ho parlato a CppCon2015: https://youtu.be/nXaxk27zwlk
Le tecniche mostrate sono fornite anche da varie librerie di micro-benchmark come Google: https://github.com/google/benchmark#preventing-optimization
La chiave di queste tecniche è concentrarsi sui dati. Rendete opaco l'input del calcolo all'ottimizzatore e il risultato del calcolo opaco all'ottimizzatore. Dopo averlo fatto, puoi calcolare il tempo in modo affidabile. Diamo un'occhiata a una versione realistica dell'esempio nella domanda originale, ma con la definizione di foo
completamente visibile all'implementazione. Ho anche estratto una versione (non portatile) di DoNotOptimize
dalla libreria Google Benchmark che puoi trovare qui: 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;
}
Qui ci assicuriamo che i dati di input e i dati di output siano contrassegnati come non ottimizzabili intorno al calcolo foo
e solo intorno a questi marker vengono calcolati i tempi. Poiché stai utilizzando i dati per tenere il calcolo, è garantito che rimanga tra i due tempi e tuttavia il calcolo stesso può essere ottimizzato. L'assembly x86-64 risultante generato da una build recente di Clang / LLVM è:
% ./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
Qui puoi vedere il compilatore che ottimizza la chiamata a foo(input)
una singola istruzione addl %eax, %eax
, ma senza spostarla al di fuori del tempo o eliminarla del tutto nonostante l'input costante.
Spero che questo aiuti e il comitato per gli standard C ++ sta esaminando la possibilità di standardizzare API simili a DoNotOptimize
qui.