2013-05-22 8 views
5

Ho eseguito un mio benchmark sul mio computer (Intel i3-3220 @ 3.3GHz, Fedora 18) e ho ottenuto risultati molto inaspettati. Un puntatore a funzione era in realtà un po 'più veloce di una funzione in linea.Il puntatore funzione viene eseguito più rapidamente della funzione inline. Perché?

Codice:

#include <iostream> 
#include <chrono> 
inline short toBigEndian(short i) 
{ 
    return (i<<8)|(i>>8); 
} 
short (*toBigEndianPtr)(short i)=toBigEndian; 
int main() 
{ 
    std::chrono::duration<double> t; 
    int total=0; 
    for(int i=0;i<10000000;i++) 
    { 
     auto begin=std::chrono::high_resolution_clock::now(); 
     short a=toBigEndian((short)i);//toBigEndianPtr((short)i); 
     total+=a; 
     auto end=std::chrono::high_resolution_clock::now(); 
     t+=std::chrono::duration_cast<std::chrono::duration<double>>(end-begin); 
    } 
    std::cout<<t.count()<<", "<<total<<std::endl; 
    return 0; 
} 

compilato con

g++ test.cpp -std=c++0x -O0 

Il ciclo 'toBigEndian' finisce sempre intorno ai 0,26-0,27 secondi, mentre 'toBigEndianPtr' prende 0.21-0.22 secondi.

Ciò che rende questo ancora più strano è che quando rimuovo 'totale', il puntatore della funzione diventa quello più lento a 0,35-0,37 secondi, mentre la funzione in linea è a circa 0,27-0,28 secondi.

La mia domanda è:

Perché il puntatore a funzione più veloce della funzione inline quando esiste 'totale'?

+5

Non stai ottimizzando. Il profiling del codice di non ottimizzazione è inutile. –

+0

Con -O3 la velocità è sempre la stessa. – Hassedev

+1

Dubito che ... –

risposta

2

Oh s ** t (devo censurare il giuramento qui?), L'ho scoperto. Era in qualche modo correlato alla tempistica di essere all'interno del ciclo. Quando l'ho spostato all'esterno come segue,

#include <iostream> 
#include <chrono> 
inline short toBigEndian(short i) 
{ 
    return (i<<8)|(i>>8); 
} 

short (*toBigEndianPtr)(short i)=toBigEndian; 
int main() 
{ 
    int total=0; 
    auto begin=std::chrono::high_resolution_clock::now(); 
    for(int i=0;i<100000000;i++) 
    { 
     short a=toBigEndianPtr((short)i); 
     total+=a; 
    } 
    auto end=std::chrono::high_resolution_clock::now(); 
    std::cout<<std::chrono::duration_cast<std::chrono::duration<double>>(end-begin).count()<<", "<<total<<std::endl; 
    return 0; 
} 

i risultati sono proprio come dovrebbero essere. 0,08 secondi per inline, 0.20 secondi per il puntatore. Scusa per averti disturbato ragazzi.

7

Risposta breve: non lo è.

  • Si compila con -O0, che non ottimizza (molto). Senza ottimizzazione, non hai parole in "veloce", perché il codice non ottimizzato non è veloce come può essere.
  • Si prende l'indirizzo di toBigEndian, che impedisce la linea. La parola chiave inline è comunque un suggerimento per il compilatore, che può seguire o meno. Hai fatto il meglio per non fallo seguire.

Così, per dare le vostre misure alcun significato,

  • ottimizzare il codice
  • uso due funzioni, facendo la stessa cosa, uno che viene inline, l'altro preso le addres di
+0

Ovviamente ho eseguito il codice più volte, quindi "in giro". I tuoi altri punti sembrano essere buoni, però. Inoltre, -O3 non sembra influenzare affatto le prestazioni. – Hassedev

+0

Credo che dovrò accettare la tua risposta. Questo è semplicemente così assurdo che non può essere correlato al programma stesso. Inoltre, non ho nemmeno bisogno di ottimizzare la funzione, in quanto non è chiamata molto spesso nel programma per cui ho bisogno. – Hassedev

+0

Ho rimosso i punti sull'esecuzione del ciclo più volte, poiché avevo alcune ipotesi errate. –

0

Prima di tutto, con -O0, non si esegue l'ottimizzatore, il che significa che il compilatore ignora la richiesta di inline, come è libero di fare. Il costo delle due diverse chiamate dovrebbe essere quasi identico. Prova con -O2.

In secondo luogo, se si esegue solo per 0,22 secondi, i costi variabili impliciti nell'avvio del programma sono totalmente dipendenti dal costo dell'esecuzione della funzione di test. Quella chiamata di funzione è solo alcune istruzioni. Se la tua CPU funziona a 2 GHz, dovrebbe eseguire quella chiamata di funzione in qualcosa come 20 nanosecondi, così puoi vedere che qualunque cosa tu stia misurando, non è il costo di eseguire quella funzione.

Provare a chiamare la funzione di test in un ciclo, diciamo 1.000.000 di volte. Rendi il numero di loop 10x più grande fino a quando non impiega più di 10 secondi per eseguire il test. Quindi dividere il risultato per il numero di cicli per un'approssimazione del costo dell'operazione.

+0

OK, l'ho fatto in loop 500.000.000 di volte. Puntatore funzione a 11,3 secondi, l'altra funzione a 13,2 secondi. – Hassedev

3

Un errore comune nella misurazione delle prestazioni (oltre a dimenticare l'ottimizzazione) consiste nell'utilizzare lo strumento sbagliato da misurare. L'uso di std :: chrono andrebbe bene se si misurassero le prestazioni dell'intero, 10000000 o 500000000 iterazioni. Invece, stai chiedendo di misurare la chiamata/inline diBigEndian. Una funzione che è composta da 6 istruzioni. Così sono passato a rdtsc (leggere il contatore dei timestamp, cioè i cicli di clock).

Consentendo al compilatore di ottimizzare veramente tutto nel ciclo, non ingombrandolo con la registrazione del tempo su ogni piccola iterazione, abbiamo una sequenza di codice diversa. Ora, dopo aver compilato con g++ -O3 fp_test.cpp -o fp_test -std=c++11, osservo l'effetto desiderato. La versione inline ha una media di circa 2,15 cicli per iterazione, mentre il puntatore della funzione richiede circa 7,0 cicli per iterazione.

Anche senza utilizzare rdtsc, la differenza è ancora abbastanza osservabile. L'ora dell'orologio a muro era 360 ms per il codice inline e 1,17 per il puntatore di funzione. Quindi si potrebbe usare std :: chrono al posto di rdtsc in questo codice.

Codice modificato segue:

#include <iostream> 
static inline uint64_t rdtsc(void) 
{ 
    uint32_t hi, lo; 
    asm volatile ("rdtsc" : "=a"(lo), "=d"(hi)); 
    return ((uint64_t)lo)|(((uint64_t)hi)<<32); 
} 
inline short toBigEndian(short i) 
{ 
    return (i<<8)|(i>>8); 
} 
short (*toBigEndianPtr)(short i)=toBigEndian; 
#define LOOP_COUNT 500000000 
int main() 
{ 
    uint64_t t = 0, begin=0, end=0; 
    int total=0; 
    begin=rdtsc(); 
    for(int i=0;i<LOOP_COUNT;i++) 
    { 
     short a=0; 
     a=toBigEndianPtr((short)i); 
     //a=toBigEndian((short)i); 
     total+=a; 
    } 
    end=rdtsc(); 
    t+=(end-begin); 
    std::cout<<((double)t/LOOP_COUNT)<<", "<<total<<std::endl; 
    return 0; 
} 
+0

Grazie per la bella idea di misurare i cicli invece dei secondi. – Hassedev

-1

Con molti/maggior parte dei compilatori moderni che si rispetti, il codice che avete inviato sarà ancora inline la funzione di chiamata anche quando quando viene chiamato tramite il puntatore. (Supponendo che il compilatore compia uno sforzo ragionevole per ottimizzare il codice). La situazione è fin troppo facile da vedere. In altre parole, il codice generato può facilmente finire praticamente lo stesso in entrambi i casi, il che significa che il test non è veramente utile per misurare ciò che stai cercando di misurare.

Se si vuole veramente assicurarsi che la chiamata sia eseguita fisicamente attraverso il puntatore, è necessario fare uno sforzo per "confondere" il compilatore fino al punto in cui non è possibile determinare il valore del puntatore al momento della compilazione. Ad esempio, impostare il valore del puntatore in base al tempo di esecuzione, come in

toBigEndianPtr = rand() % 1000 != 0 ? toBigEndian : NULL; 

o qualcosa del genere. Puoi anche dichiarare il tuo puntatore di funzione come volatile, che di solito causa ogni volta una vera chiamata attraverso il puntatore e impone al compilatore di rileggere il valore del puntatore dalla memoria ad ogni iterazione.