2011-01-24 13 views
7

Oggi ho letto il codice utilizzando una tabella di ricerca anziché if-else per il ritaglio di due valori uint8 sommati. La mappa è in i={0...255} e 255 in i={256...511}. Mi chiedevo quanto è grande il guadagno di questo potrebbe essere, e ho cercato di scoprirlo, con gprof,Lookup Table vs if-else

g++ -std=c++0x -pg perfLookup.cpp -O2 -o perfLookup && ./perfLookup && gprof perfLookup |less 

con il codice allegato qui sotto. Ora senza il flag -O2 gprof dice che lookup() prende il 45% e ifelse() come il 48% del tempo di esecuzione. Con -O2 anche se è 56% per lookup() e 43% per ifelse(). Ma questo benchmark è veramente corretto? Forse un sacco di codice è ottimizzato perché il dst non viene mai letto?

#include <iostream> 
#include <cstdint> 
#include <vector> 

void lookup(std::vector<uint8_t> src, int repeat) { 
    uint8_t lookup[511]; 
    for (int i = 0; i < 256; i++) { 
    lookup[i] = i; 
    } 
    for (int i = 256; i < 512; i++) { 
    lookup[i] = 255; 
    } 

    std::vector<uint8_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int i = 0; i < src.size(); i++) { 
     dst[i] = lookup[src[i]]; 
    } 
    } 

} 

void ifelse(std::vector<uint8_t> src, int repeat) { 
    std::vector<uint8_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int i = 0; i < src.size(); i++) { 
     dst[i] = (src[i] > 255) ? 255 : src[i]; 
    } 
    } 
} 

int main() 
{ 
    int n = 10000; 
    std::vector<uint8_t> src(n); 
    for (int i = 0; i < src.size(); i++) { 
    src[i] = rand() % 510; 
    } 

    lookup(src, 10000); 
    ifelse(src, 10000); 
} 

codice aggiornato:

#include <iostream> 
#include <cstdint> 
#include <cstring> 
#include <vector> 
#include <algorithm> 

// g++ -std=c++0x -pg perfLookup.cpp -O2 -o perfLookup && ./perfLookup && gprof perfLookup |less 

std::vector<uint16_t> lookup(std::vector<uint16_t> src, int repeat) { 
    uint16_t lookup[511]; 
    for (int i = 0; i < 256; i++) { 
    lookup[i] = i; 
    } 
    for (int i = 256; i < 511; i++) { 
    lookup[i] = 255; 
    } 

    std::vector<uint16_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int k = 0; k < src.size(); k++) { 
     dst[k] = lookup[src[k]]; 
    } 
    } 

    return dst; 

} 

std::vector<uint16_t> ifelse(std::vector<uint16_t> src, int repeat) { 
    std::vector<uint16_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int k = 0; k < src.size(); k++) { 
     dst[k] = (src[k] > 255) ? 255 : src[k]; 
    } 
    } 
    return dst; 
} 

std::vector<uint16_t> copyv(std::vector<uint16_t> src, int repeat) { 
    std::vector<uint16_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    dst = src; 
    for (int k = 0; k < src.size(); k++) { 
     if (dst[k] > 255) { 
    dst[k] = 255; 
     } 
    } 
    } 
    return dst; 
} 

std::vector<uint16_t> copyC(std::vector<uint16_t> src, int repeat) 
{ 
    uint16_t* dst = (uint16_t *) malloc(sizeof(uint16_t) * src.size()); // Alloc array for dst 

    for (int i = 0; i < repeat; i++) { 
    std::memcpy(dst, &src[0], sizeof(uint16_t) * src.size()); // copy src into array 

    for (int k = 0; k < src.size(); k++) { 
     if ((dst[k] & 0xFF00) != 0) 
    dst[k] = 0x00FF; 
    } 
    } 

    free(dst); 
    return std::vector<uint16_t>(); 
} 

int main() 
{ 
    int n = 10000; 
    std::vector<uint16_t> src(n); 
    for (int i = 0; i < src.size(); i++) { 
    src[i] = rand() % 510; 
    } 
    std::vector<uint16_t> dst; 
    dst = lookup(src, 10000); 
    dst = ifelse(src, 10000); 
    dst = copyv(src, 10000); 
} 
+3

nota che si sta misurando l'inizializzazione della tabella di ricerca come parte della vostra benchmarking. Normalmente si inizializza una tabella di ricerca separatamente e non la si include nel benchmarking. –

+0

Non includerei l'inizializzazione della tabella di ricerca nella funzione mesured perché ciò può essere eseguito una sola volta durante l'esecuzione di un programma. –

+1

Alcune modifiche apportate al codice: utilizzare l'argomento 'src' ed eseguire il clipping sul posto --note che questa è già una copia, non un riferimento all'originale. Restituisci quel vettore dalla funzione, altrimenti il ​​compilatore potrebbe rimuovere tutto il codice dalle funzioni poiché la variabile locale non viene mai utilizzata. Crea e archivia la tabella di ricerca al di fuori del codice di test, senza aggiungere operazioni che non influiranno sul risultato. –

risposta

7

Ebbene, dal momento che src è dichiarato come std::vector<uint8_t>, src[i] è mai più grande di 255, che è il valore più alto possibile per un numero intero senza segno a 8 bit.

Pertanto, la mia ipotesi è che il compilatore ottimizzi il controllo. Ciò che rimane è solo il loop boilerplate, quindi il benchmark non ha senso.

A condizione che il controllo non fosse privo di significato (vale a dire contro 64 anziché 255), il risultato dell '"ottimizzazione" sarà presumibilmente altamente dipendente dalla macchina. La previsione del ramo può (a seconda dei dati di input) fare un buon lavoro nel ridurre il costo del ramo. D'altra parte la tabella di ricerca ha bisogno (sempre a seconda dei dati di input) dell'accesso casuale alla memoria e rovina la cache ...

+0

Buon punto! Dopo aver cambiato tutto in uint16_t ifelse() è al 58% e lookup() è al 42%. Quindi, in effetti, il compilatore era abbastanza intelligente. (A -O3 ottimizza anche entrambe le chiamate di funzione, più o meno.) La distribuzione rimane stabile anche quando si modifica il numero di cicli, per quanto riguarda la costruzione della tabella di ricerca. Mi chiedo come questo possa apparire nel sistema «reale» (effetto di editing video, quindi anche molti altri accessi alla cache) ... –

2

Si sta misurando anche il tempo per l'inizializzazione della tabella di ricerca, e questo potrebbe non Sii quel che vuoi essere. Se la tabella viene inizializzata solo una volta nel codice di produzione, ma viene utilizzata molte volte, non è necessario misurare l'inizializzazione.

+0

D'accordo, vorrei prendere l'inizializzazione della tabella di ricerca dalla funzione 'cerca '. Si potrebbe anche digitare l'inizializzazione di esso e renderlo const (ad esempio const uint8_t lookup [] = {0, 1, 2, 3 ...}) – GrahamS

+0

Sono d'accordo; In questo caso, la costruzione della LUT è stata abbastanza veloce anche se non ha praticamente alcun effetto (specialmente quando si esegue il ciclo molte volte). –

7

A parte la cosa Alexander ha già detto:

tabelle di ricerca possono migliorare le prestazioni drasticamente. Tuttavia, questo è compensato dal tempo necessario per creare la tabella di ricerca in primo luogo. Solitamente lo si valuterà separatamente.

Un'altra cosa da tenere a mente è che la tabella di ricerca richiede spazio nella cache e può quindi causare errori di cache se è grande. Se ci sono abbastanza errori di cache, il metodo if sarà più veloce della tabella di ricerca.

Infine, gprof è molto utile per identificare i colli di bottiglia. Ma non lo userei per i benchmark. Utilizzare invece una funzione di temporizzazione. gprof utilizza il campionamento che può essere mappato strettamente al tempo consumato, ma qui è meno preciso.

+0

Grazie per i suggerimenti! Per quanto riguarda le funzioni di cronometraggio, cosa stai pensando di qui? Tempo di orologio da parete? O cicli della CPU? –

+0

@Simon: letteralmente non importa, una volta eseguite iterazioni sufficienti (ad es. Lunghezza del ciclo individuale> 1 s) e ripetizioni sufficienti dell'esperimento. Generalmente, migliore è la risoluzione, migliore è il risultato. Ma con tutti gli altri fattori che invariabilmente influenzano i benchmark, non mettere troppa importanza nell'orologio. –

3

La gestione dell'array lookup è danneggiata. Questa linea:

uint8_t lookup[511]; 

è spento per uno, si vuole lookup[512]; dal momento che sembra aspettarsi di indice con 511 (che accede l'elemento 512th). Naturalmente, come ha sottolineato Alexander, è tutto sommato lo stesso dal uint8_t significa che non puoi avere un indice di qualcosa sopra 255.

Orbene, questo codice:

for (int i = 256; i < 512; i++) { 
    lookup[i] = 255; 
} 

indice volontà fuori limite e scrivere 255 ad una locazione di memoria più o meno scelto a caso.

+0

Grazie, è giusto. Dovrebbe significare che <511 lì. 511 non dovrebbe verificarsi poiché sto aggiungendo due numeri a 8 bit, il massimo è 255 + 255 = 510, cioè 511 possibili risultati. –

+0

@Simon: si verifica sicuramente, ho aggiunto il codice in errore. – unwind

+0

Sì, quindi «i <511»;) Ho fatto un errore con il limite superiore lì. –

1

A volte il compilatore è abbastanza intelligente per ottimizzare le semplici prove di profiling. Se questo è il caso, hai il trucco che il compilatore non sta ottimizzando. L'uso di un valore di ripetizione molto più ampio può anche aiutarti a ottenere risultati migliori o dirti se qualcosa viene ottimizzato.

tabelle di ricerca può essere più veloce di incatenato se/elseifs ma in questo caso con un solo confronto non mi aspetterei molta differenza. Ad esempio, se avessi 10, 100, 1000 ... confronti la tabella di ricerca dovrebbe generalmente vincere.

+0

Lui sta facendo 10000 confronti. – GrahamS

+0

Intendevo avere molti confronti nello stesso se ... blocco elseif (cioè uno se seguito da 1000 elseifs). – uesp

2

Entrambi gli approcci sembrano piuttosto strano. Hai davvero bisogno di questo livello di ottimizzazione? Se è così allora vorrei mettere in dubbio l'uso dei vettori e prendere in considerazione i mazzi C invece!

L'approccio "IfElse" sembra più evidente. Dubito che è notevolmente più lento/veloce rispetto alla tabella di ricerca a meno che non si sta chiamando questo miliardi di volte.

Personalmente avrei probabilmente solo clonare il vettore src poi scorrere su di esso e fissare i valori (utilizzando 250 qui perché 255 non ha senso come ha sottolineato):

std::vector<uint8_t> dst(src); 
for(std::vector<int>::size_type i = 0; i != v.size(); i++) 
{ 
    if (dst[i] > 250) dst[i] = 250; 
} 

A seconda di come la clonazione viene effettivamente eseguita e ottimizzato dal compilatore (ad esempio può fare una copia di memoria a blocchi), questo potrebbe effettivamente essere leggermente più veloce. È certamente più ordinato e più facile da capire.

+0

Un approccio simile potrebbe riservare lo spazio per il secondo vettore - senza eseguire le copie effettive e quindi 'std :: transform (v.begin(), v.end(), [] (uint8_t x) {return std :: min (x, 250)}); '(usando la sintassi lambda per compattezza) –

+0

Sì, questo livello è richiesto. Effetto video → ogni percentuale conta :) Mi è stato detto che l'accesso agli array è altrettanto veloce dell'accesso vettoriale. Ho provato a clonare, ma sembra essere molto più lento (a meno che non stia facendo qualcosa di sbagliato); confronta l'80% con la funzione copyv(), il 12% con ifelse() e l'8% con lookup(). Aggiungerà il codice aggiornato nella domanda iniziale. –

+0

@David posso usare la sintassi Lambda nel codice proprio come nel tuo esempio, o è questo C++ valido per il nod? (Ho provato e ottenere errori del compilatore) –

1

Una possibile soluzione C po 'sporco (fuori dalla parte superiore della mia testa e non testato/non compilato, quindi probabilmente contiene errori):

std::vector<uint16_t> copyC(std::vector<uint16_t> src, int repeat) 
{ 
    uint16_t* dst = malloc(sizeof(unit16_t) * src.size()); // Alloc array for dst 

    for (int i = 0; i < repeat; i++) 
    { 
     memcpy(dst, &src[0], sizeof(unit16_t) * src.size()); // copy src into array 

     for (int k = 0; k < src.size(); k++) 
     { 
      if ((dst[k] & 0xFF00) != 0) 
       dst[k] = 0x00FF; 
     } 
    } 

    free(dst); 
} 

Sarei interessato a vedere come che confronta. (Anche in questo caso può dipendere dall'implementazione di memcpy, poiché sarà più veloce se le copie di memoria di grandi dimensioni sono più efficienti delle copie byte per byte).

base alle specifiche del chip (cioè 8 bit o 16 bit dimensioni del registro), accesso a byte singolo potrebbe essere più veloce rispetto al doppio byte. se è così allora il codice sopra potrebbe anche essere riscritto per trattare dst come una matrice di unit8_t. Quindi esaminerebbe solo ogni secondo byte e se era diverso da zero lo imposta a 0 e il seguente byte * a 0xFF.

(* o byte precedente, a seconda endianness)

+0

44,4% per copyv, 43,9% per copyC, 6,7% per ifelse, 5% per la ricerca (tutti eseguiti consecutivamente). Compilato con -O2, ripetizioni di 100k. Se cambio il codice copyv per usare & 0xFF00, è più veloce di circa l'1%. Quindi la velocità tra questi due sembra essere uguale. –

+0

@Simon A. Eugster: un altro risultato interessante. Sembra che la memcpy in 'copyC' e la copia vettoriale in' copyV' stiano uccidendo le loro prestazioni. Interessante quanto è grande questo vettore 'src' e su quale piattaforma stai usando questo (chip e sistema operativo)? Inoltre, l'uso di '& 0xFF00' velocizza la tua soluzione ifelse? – GrahamS

+0

(Oh scusa, non ho fatto scorrere abbastanza lontano. Vedo che src è 10.000 * uint16_t) – GrahamS