Applicazione dell'ordine delle istruzioni in C ++


111

Supponiamo che io abbia un numero di istruzioni che voglio eseguire in un ordine fisso. Voglio usare g ++ con il livello di ottimizzazione 2, quindi alcune istruzioni potrebbero essere riordinate. Quali strumenti sono disponibili per imporre un certo ordine di affermazioni?

Considera il seguente esempio.

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 questo esempio è importante che le istruzioni 1-3 siano eseguite nell'ordine dato. Tuttavia, il compilatore non può pensare che l'istruzione 2 sia indipendente da 1 e 3 ed eseguire il codice come segue?

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;

34
Se il compilatore pensa di essere indipendenti quando non lo sono, il compilatore è guasto e dovresti usare un compilatore migliore.
David Schwartz


1
potrebbe __sync_synchronize()essere di aiuto?
vsz

3
@HowardHinnant: La potenza semantica dello standard C sarebbe migliorata enormemente se una tale direttiva fosse definita e se le regole di aliasing fossero adattate per esentare le letture eseguite dopo una barriera di dati che è stata scritta prima di essa.
supercat

4
@DavidSchwartz In questo caso si tratta di misurare il tempo foonecessario per l'esecuzione, che il compilatore può ignorare durante il riordino, proprio come è consentito ignorare l'osservazione da un thread diverso.
CodesInChaos

Risposte:


100

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 foocompletamente visibile all'implementazione. Ho anche estratto una versione (non portatile) di DoNotOptimizedalla 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 fooe 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 DoNotOptimizequi.


1
La ringrazio per la risposta. L'ho contrassegnata come la nuova migliore risposta. Avrei potuto farlo prima, ma non leggo questa pagina dello stackoverflow da molti mesi. Sono molto interessato a utilizzare il compilatore Clang per creare programmi C ++. Tra le altre cose, mi piace che si possano usare caratteri Unicode nei nomi delle variabili in Clang. Penso che farò altre domande su Clang su Stackoverflow.
S2108887

5
Anche se capisco come questo impedisce che foo venga completamente ottimizzato, puoi spiegare un po 'perché questo impedisce alle chiamate di Clock::now()essere riordinate rispetto a foo ()? L'ottimizzatore deve assumerlo DoNotOptimizee Clock::now()avere accesso e potrebbe modificare alcuni stati globali comuni che a loro volta li legherebbero all'ingresso e all'uscita? O ti stai affidando ad alcune attuali limitazioni dell'implementazione dell'ottimizzatore?
MikeMB

2
DoNotOptimizein questo esempio è un evento sinteticamente "osservabile". È come se stampasse teoricamente un output visibile su un terminale con la rappresentazione dell'input. Poiché la lettura dell'orologio è anche osservabile (stai osservando il tempo che passa) non possono essere riordinati senza modificare il comportamento osservabile del programma.
Chandler Carruth

1
Non sono ancora abbastanza chiaro con il concetto di "osservabile", se la foofunzione sta eseguendo alcune operazioni come la lettura da un socket che potrebbe essere bloccato per un po ', conta un'operazione osservabile? E poiché l'operazione readnon è "totalmente nota" (giusto?), Il codice manterrà l'ordine?
ravenisadesk

"Il problema fondamentale è che la semantica operazionale di qualcosa come un'addizione intera è totalmente nota all'implementazione." Ma mi sembra che il problema non sia la semantica dell'addizione di interi, ma la semantica della chiamata alla funzione foo (). A meno che foo () non sia nella stessa unità di compilazione, come fa a sapere che foo () e clock () non interagiscono?
Dave

59

Sommario:

Non sembra esserci un modo garantito per impedire il riordino, ma finché l'ottimizzazione del tempo di collegamento / programma completo non è abilitata, individuare la funzione chiamata in un'unità di compilazione separata sembra una scommessa abbastanza buona . (Almeno con GCC, sebbene la logica suggerisca che questo è probabile anche con altri compilatori.) Ciò viene a costo della chiamata di funzione - il codice inline è per definizione nella stessa unità di compilazione e aperto al riordino.

Risposta originale:

GCC riordina le chiamate sotto l'ottimizzazione -O2:

#include <chrono>
static int foo(int x)    // 'static' or not here doesn't affect ordering.
{
    return x*2;
}
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

GCC 5.3.0:

g++ -S --std=c++11 -O0 fred.cpp :

_ZL3fooi:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %ecx, 16(%rbp)
        movl    16(%rbp), %eax
        addl    %eax, %eax
        popq    %rbp
        ret
_Z4fredi:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $64, %rsp
        movl    %ecx, 16(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -16(%rbp)
        movl    16(%rbp), %ecx
        call    _ZL3fooi
        movl    %eax, -4(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -32(%rbp)
        movl    -4(%rbp), %eax
        addq    $64, %rsp
        popq    %rbp
        ret

Ma:

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        call    _ZNSt6chrono3_V212system_clock3nowEv
        leal    (%rbx,%rbx), %eax
        addq    $32, %rsp
        popq    %rbx
        ret

Ora, con foo () come funzione esterna:

#include <chrono>
int foo(int x);
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %ecx
        call    _Z3fooi
        movl    %eax, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %eax
        addq    $32, %rsp
        popq    %rbx
        ret

MA, se questo è collegato con -flto (ottimizzazione del tempo di collegamento):

0000000100401710 <main>:
   100401710:   53                      push   %rbx
   100401711:   48 83 ec 20             sub    $0x20,%rsp
   100401715:   89 cb                   mov    %ecx,%ebx
   100401717:   e8 e4 ff ff ff          callq  100401700 <__main>
   10040171c:   e8 bf f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401721:   e8 ba f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401726:   8d 04 1b                lea    (%rbx,%rbx,1),%eax
   100401729:   48 83 c4 20             add    $0x20,%rsp
   10040172d:   5b                      pop    %rbx
   10040172e:   c3                      retq

3
Così fa MSVC e ICC. Clang è l'unico che sembra preservare la sequenza originale.
Cody Grey

3
non usi t1 e t2 da nessuna parte, quindi potresti pensare che il risultato possa essere scartato e riordinare il codice
phuclv

3
@ Niall - Non posso offrire niente di più concreto, ma penso che il mio commento alluda al motivo sottostante: il compilatore sa che foo () non può influenzare now (), né viceversa, e così fa il riordino. Vari esperimenti che coinvolgono funzioni e dati esterni sembrano confermarlo. Ciò include il fatto che foo () statico dipenda da una variabile di ambito del file N - se N è dichiarato come statico, si verifica il riordino, mentre se è dichiarato non statico (cioè è visibile ad altre unità di compilazione, e quindi potenzialmente soggetto agli effetti collaterali di funzioni extern come il riordino now ()) non si verifica.
Jeremy

3
@ Lưu Vĩnh Phúc: Tranne che le chiamate stesse non sono elide. Ancora una volta, ho il sospetto che questo è perché il compilatore non sa quali potrebbero essere i loro effetti collaterali - ma non so che questi effetti collaterali non possono influenzare il comportamento di foo ().
Jeremy

3
E una nota finale: specificando -flto (ottimizzazione del tempo di collegamento) si causa il riordino anche in casi altrimenti non riordinati.
Jeremy

20

Il riordino può essere eseguito dal compilatore o dal processore.

La maggior parte dei compilatori offre un metodo specifico della piattaforma per impedire il riordino delle istruzioni di lettura e scrittura. Su gcc, questo è

asm volatile("" ::: "memory");

( Maggiori informazioni qui )

Si noti che questo impedisce solo indirettamente le operazioni di riordino, a condizione che dipendono dalle letture / scritture.

In pratica non ho ancora visto un sistema in cui la chiamata di sistema Clock::now()abbia lo stesso effetto di tale barriera. Potresti ispezionare l'assieme risultante per essere sicuro.

Non è raro, tuttavia, che la funzione sotto test venga valutata durante la fase di compilazione. Per applicare un'esecuzione "realistica", potrebbe essere necessario derivare l'input dall'I foo()/ O o da una volatilelettura.


Un'altra opzione sarebbe disabilitare l'inlining per foo()- ancora una volta, questo è specifico del compilatore e di solito non portabile, ma avrebbe lo stesso effetto.

Su gcc, questo sarebbe __attribute__ ((noinline))


@Ruslan solleva una questione fondamentale: quanto è realistica questa misurazione?

Il tempo di esecuzione è influenzato da molti fattori: uno è l'hardware effettivo su cui stiamo lavorando, l'altro è l'accesso simultaneo a risorse condivise come cache, memoria, disco e core della CPU.

Quindi cosa facciamo di solito per ottenere tempi comparabili : assicurati che siano riproducibili con un margine di errore basso. Questo li rende in qualche modo artificiali.

Le prestazioni di esecuzione di "cache calda" e "cache fredda" possono facilmente differire di un ordine di grandezza, ma in realtà sarà qualcosa di intermedio ("tiepido"?)


2
Il tuo hack asminfluisce sul tempo di esecuzione delle istruzioni tra le chiamate del timer: il codice dopo il memory clobber deve ricaricare tutte le variabili dalla memoria.
Ruslan

@Ruslan: il loro hack, non il mio. Esistono diversi livelli di eliminazione e fare qualcosa del genere è inevitabile per ottenere risultati riproducibili.
Peterchen

2
Nota che l'hack con 'asm' aiuta solo come barriera per le operazioni che toccano la memoria, e l'OP è interessato a qualcosa di più. Vedi la mia risposta per maggiori dettagli.
Chandler Carruth,

11

Il linguaggio C ++ definisce ciò che è osservabile in diversi modi.

Se foo()non fa nulla di osservabile, allora può essere eliminato completamente. Se foo()solo esegue un calcolo che memorizza i valori nello stato "locale" (sia nello stack che in un oggetto da qualche parte) e il compilatore può provare che nessun puntatore derivato in modo sicuro può entrare nel Clock::now()codice, allora non ci sono conseguenze osservabili da spostare le Clock::now()chiamate.

Se foo()interagito con un file o il display, e il compilatore non può dimostrare che Clock::now()fa non interagire con il file o il display, quindi riordino non può essere fatto, perché l'interazione con un file o display è comportamento osservabile.

Sebbene sia possibile utilizzare hack specifici del compilatore per forzare il codice a non spostarsi (come l'assembly inline), un altro approccio è tentare di superare in astuzia il compilatore.

Crea una libreria caricata dinamicamente. Caricalo prima del codice in questione.

Quella libreria espone una cosa:

namespace details {
  void execute( void(*)(void*), void *);
}

e lo avvolge in questo modo:

template<class F>
void execute( F f ) {
  struct bundle_t {
    F f;
  } bundle = {std::forward<F>(f)};

  auto tmp_f = [](void* ptr)->void {
    auto* pb = static_cast<bundle_t*>(ptr);
    (pb->f)();
  };
  details::execute( tmp_f, &bundle );
}

che impacchetta un lambda nullo e utilizza la libreria dinamica per eseguirlo in un contesto che il compilatore non può comprendere.

All'interno della libreria dinamica, facciamo:

void details::execute( void(*f)(void*), void *p) {
  f(p);
}

che è abbastanza semplice.

Ora per riordinare le chiamate a execute, deve comprendere la libreria dinamica, che non può durante la compilazione del codice di test.

Può ancora eliminare foo()i messaggi di posta elettronica con zero effetti collaterali, ma alcuni ne vinci, altri ne perdi.


19
"un altro approccio è tentare di superare in astuzia il compilatore" Se quella frase non è un segno di essere andato nella tana del coniglio, non so cosa sia. :-)
Cody Grey

1
Penso che potrebbe essere utile notare che il tempo richiesto per l'esecuzione di un blocco di codice non è considerato un comportamento "osservabile" che i compilatori devono mantenere . Se il tempo necessario per eseguire un blocco di codice fosse "osservabile", nessuna forma di ottimizzazione delle prestazioni sarebbe consentita. Sebbene sarebbe utile per C e C ++ definire una "barriera di causalità" che richiederebbe a un compilatore di trattenere l'esecuzione di qualsiasi codice dopo la barriera fino a quando tutti gli effetti collaterali di prima che la barriera fosse stata gestita dal codice generato [codice che vuole garantire che i dati siano completamente ...
supercat

1
... propagato attraverso le cache hardware avrebbe bisogno di utilizzare mezzi specifici per l'hardware per farlo, ma un mezzo specifico per l'hardware di attesa fino al completamento di tutte le scritture postate sarebbe inutile senza una direttiva barriera per garantire che tutte le scritture in sospeso tracciate dal compilatore deve essere inviato all'hardware prima che venga richiesto all'hardware di assicurarsi che tutte le scritture inviate siano complete.] Non conosco alcun modo per farlo in nessuna delle due lingue senza utilizzare un volatileaccesso fittizio o chiamare un codice esterno.
supercat

4

No, non può. Secondo lo standard C ++ [intro.execution]:

14 Ogni calcolo di valore ed effetto collaterale associato a un'espressione completa viene sequenziato prima di ogni calcolo di valore ed effetto collaterale associato all'espressione completa successiva da valutare.

Un'espressione completa è fondamentalmente un'istruzione terminata da un punto e virgola. Come puoi vedere, la regola precedente stabilisce che le istruzioni devono essere eseguite in ordine. È all'interno delle istruzioni che al compilatore è concesso più libero sfogo (cioè, in alcune circostanze è consentito valutare espressioni che compongono un'istruzione in ordini diversi da sinistra a destra o qualsiasi altra cosa specifica).

Nota che le condizioni per l'applicazione della regola as-if non sono soddisfatte qui. Non è ragionevole pensare che qualsiasi compilatore sarebbe in grado di dimostrare che il riordino delle chiamate per ottenere l'ora di sistema non influirebbe sul comportamento osservabile del programma. Se ci fosse una circostanza in cui due chiamate per ottenere l'ora potrebbero essere riordinate senza modificare il comportamento osservato, sarebbe estremamente inefficiente produrre effettivamente un compilatore che analizzi un programma con sufficiente comprensione per poter dedurre questo con certezza.


12
Tuttavia, c'è ancora la regola del come se
MM

18
Il compilatore di regole as-if può fare qualsiasi cosa sul codice purché non cambi il comportamento osservabile. Il tempo di esecuzione non è osservabile. Quindi può riordinare righe di codice arbitrarie purché il risultato sia lo stesso (la maggior parte dei compilatori fa cose sensate e non riordina le chiamate temporali, ma non è richiesto)
Revolver_Ocelot

6
Il tempo di esecuzione non è osservabile. Questo è abbastanza strano. Da un punto di vista pratico e non tecnico, il tempo di esecuzione (noto anche come "performance") è molto osservabile.
Frédéric Hamidi

3
Dipende da come misuri il tempo. Non è possibile misurare il numero di cicli di clock necessari per eseguire parte del codice in C ++ standard.
Peter

3
@dba Stai mescolando alcune cose insieme. Il linker non può più generare applicazioni Win16, questo è abbastanza vero, ma è perché hanno rimosso il supporto per la generazione di quel tipo di binario. Le app WIn16 non utilizzano il formato PE. Ciò non implica che il compilatore o il linker abbiano una conoscenza speciale delle funzioni API. L'altro problema è relativo alla libreria runtime. Non c'è assolutamente alcun problema a ottenere l'ultima versione di MSVC per generare un binario che gira su NT 4. L'ho fatto. Il problema nasce non appena si tenta di collegarsi al CRT, che chiama funzioni non disponibili.
Cody Grey

2

No.

A volte, secondo la regola "come se", le istruzioni possono essere riordinate. Questo non perché sono logicamente indipendenti l'uno dall'altro, ma perché tale indipendenza consente che tale riordinamento avvenga senza modificare la semantica del programma.

Lo spostamento di una chiamata di sistema che ottiene l'ora corrente ovviamente non soddisfa tale condizione. Un compilatore che lo fa consapevolmente o inconsapevolmente è non conforme e davvero sciocco.

In generale, non mi aspetterei che nessuna espressione che si traduca in una chiamata di sistema venga "indovinata" nemmeno da un compilatore aggressivo. Semplicemente non sa abbastanza su cosa fa quella chiamata di sistema.


5
Sono d'accordo che sarebbe sciocco, ma non lo definirei non conforme . Il compilatore può sapere cosa fa esattamente la chiamata di sistema sul sistema concreto e se ha effetti collaterali. Mi aspetto che i compilatori non riordinino tale chiamata solo per coprire casi d'uso comuni, consentendo una migliore esperienza utente, non perché lo standard lo proibisca.
Revolver_Ocelot

4
@Revolver_Ocelot: le ottimizzazioni che cambiano la semantica del programma (ok, tranne che per l'elisione della copia) non sono conformi allo standard, che tu sia d'accordo o meno.
Gare di leggerezza in orbita

6
Nel caso banale int x = 0; clock(); x = y*2; clock();ci sono presenti modi definiti per il clock()codice di interagire con lo stato di x. Secondo lo standard C ++, non deve sapere cosa clock()fa - potrebbe esaminare lo stack (e notare quando avviene il calcolo), ma questo non è un problema di C ++ .
Yakk - Adam Nevraumont

5
Per approfondire il punto di Yakk: è vero che riordinare le chiamate di sistema, in modo che il risultato della prima sia assegnato t2e del secondo a t1, sarebbe non conforme e sciocco se si usassero quei valori, ciò che manca a questa risposta è che un compilatore conforme può a volte riordinare altro codice attraverso una chiamata di sistema. In questo caso, a condizione che sappia cosa foo()fa (ad esempio perché lo ha allineato) e quindi (in senso lato) è una funzione pura, allora può spostarlo.
Steve Jessop

1
.. ancora in modo approssimativo, questo perché non c'è garanzia che l'effettiva implementazione (anche se non la macchina astratta) non calcolerà speculativamente y*yprima della chiamata di sistema, solo per divertimento. Non vi è inoltre alcuna garanzia che l'effettiva implementazione non utilizzerà il risultato di questo calcolo speculativo in un secondo momento in qualsiasi momento xvenga utilizzato, quindi non facendo nulla tra le chiamate a clock(). Lo stesso vale per qualsiasi cosa faccia una funzione inline foo, a condizione che non abbia effetti collaterali e non possa dipendere dallo stato che potrebbe essere alterato clock().
Steve Jessop

0

noinline funzione + scatola nera assembly inline + dipendenze dati complete

Questo è basato su https://stackoverflow.com/a/38025837/895245 ma poiché non ho visto alcuna chiara giustificazione del motivo per cui ::now()non può essere riordinato lì, preferirei essere paranoico e inserirlo in una funzione noinline insieme al asm.

In questo modo sono abbastanza sicuro che il riordino non possa avvenire, poiché noinline"lega" la ::nowe la dipendenza dai dati.

main.cpp

#include <chrono>
#include <iostream>
#include <string>

// noinline ensures that the ::now() cannot be split from the __asm__
template <class T>
__attribute__((noinline)) auto get_clock(T& value) {
    // Make the compiler think we actually use / modify the value.
    // It can't "see" what is going on inside the assembly string.
    __asm__ __volatile__ ("" : "+g" (value));
    return std::chrono::high_resolution_clock::now();
}

template <class T>
static T foo(T niters) {
    T result = 42;
    for (T i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    return result;
}

int main(int argc, char **argv) {
    unsigned long long input;
    if (argc > 1) {
        input = std::stoull(argv[1], NULL, 0);
    } else {
        input = 1;
    }

    // Must come before because it could modify input
    // which is passed as a reference.
    auto t1 = get_clock(input);
    auto output = foo(input);
    // Must come after as it could use the output.
    auto t2 = get_clock(output);
    std::cout << "output " << output << std::endl;
    std::cout << "time (ns) "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()
              << std::endl;
}

GitHub a monte .

Compila ed esegui:

g++ -ggdb3 -O3 -std=c++14 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out 1000
./main.out 10000
./main.out 100000

L'unico piccolo svantaggio di questo metodo è che aggiungiamo callqun'istruzione extra a un inlinemetodo. objdump -CDmostra che maincontiene:

    11b5:       e8 26 03 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>
    11ba:       48 8b 34 24             mov    (%rsp),%rsi
    11be:       48 89 c5                mov    %rax,%rbp
    11c1:       b8 2a 00 00 00          mov    $0x2a,%eax
    11c6:       48 85 f6                test   %rsi,%rsi
    11c9:       74 1a                   je     11e5 <main+0x65>
    11cb:       31 d2                   xor    %edx,%edx
    11cd:       0f 1f 00                nopl   (%rax)
    11d0:       48 8d 48 fd             lea    -0x3(%rax),%rcx
    11d4:       48 83 c2 01             add    $0x1,%rdx
    11d8:       48 0f af c1             imul   %rcx,%rax
    11dc:       48 83 c0 01             add    $0x1,%rax
    11e0:       48 39 d6                cmp    %rdx,%rsi
    11e3:       75 eb                   jne    11d0 <main+0x50>
    11e5:       48 89 df                mov    %rbx,%rdi
    11e8:       48 89 44 24 08          mov    %rax,0x8(%rsp)
    11ed:       e8 ee 02 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>

quindi vediamo che fooera inline, ma get_clocknon lo erano e lo circondano.

get_clock stesso tuttavia è estremamente efficiente, costituito da una singola istruzione ottimizzata per la chiamata foglia che non tocca nemmeno lo stack:

00000000000014e0 <auto get_clock<unsigned long long>(unsigned long long&)>:
    14e0:       e9 5b fb ff ff          jmpq   1040 <std::chrono::_V2::system_clock::now()@plt>

Poiché la precisione dell'orologio è di per sé limitata, penso che sia improbabile che tu possa notare gli effetti temporali di uno in più jmpq. Notare che uno callè richiesto indipendentemente dal momento che si ::now()trova in una libreria condivisa.

Chiamata ::now()da un assembly inline con una dipendenza dai dati

Questa sarebbe la soluzione più efficiente possibile, superando anche gli extra di jmpqcui sopra.

Questo è purtroppo estremamente difficile da eseguire correttamente, come mostrato in: Chiamare printf in ASM inline esteso

Tuttavia, se la misurazione del tempo può essere eseguita direttamente nell'assemblaggio in linea senza una chiamata, è possibile utilizzare questa tecnica. Questo è il caso, ad esempio, delle istruzioni di strumentazione magica gem5 , RDTSC x86 (non sono sicuro che sia più rappresentativo) e possibilmente altri contatori delle prestazioni.

Discussioni correlate:

Testato con GCC 8.3.0, Ubuntu 19.04.


1
Normalmente non è necessario forzare una fuoriuscita / ricarica con "+m", l'uso "+r"è un modo molto più efficiente per far materializzare un valore al compilatore e quindi assumere che la variabile sia cambiata.
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.