2010-03-31 15 views
82

Sto eseguendo alcune operazioni critiche sulle prestazioni in C++ e attualmente utilizziamo calcoli integer per problemi che sono intrinsecamente a virgola mobile perché "più veloce". Questo causa un sacco di fastidiosi problemi e aggiunge un sacco di codice fastidioso.Calcoli in virgola mobile vs integer su hardware moderno

Ora, mi ricordo di aver letto su come i calcoli in virgola mobile sono stati così lenti approssimativamente circa i 386 giorni, dove credo (IIRC) che ci fosse un co-Processore opzionale. Ma sicuramente al giorno d'oggi con CPU esponenzialmente più complesse e potenti non fa differenza in "velocità" se si eseguono calcoli in virgola mobile o interi? Soprattutto perché il tempo di calcolo effettivo è minuscolo rispetto a qualcosa di simile a causare uno stallo della pipeline o il recupero di qualcosa dalla memoria principale?

So che la risposta corretta è quella di eseguire il benchmark sull'hardware di destinazione, quale sarebbe un buon modo per testarlo? Ho scritto due piccoli programmi C++ e ho confrontato il loro tempo di esecuzione con "time" su Linux, ma il tempo di esecuzione effettivo è troppo variabile (non aiuta a funzionare su un server virtuale). A parte passare tutto il giorno a gestire centinaia di benchmark, fare grafici, ecc. C'è qualcosa che posso fare per ottenere un ragionevole test della velocità relativa? Qualche idea o pensiero? Ho completamente torto?

I programmi che ho usato come segue, essi non sono identici con qualsiasi mezzo:

#include <iostream> 
#include <cmath> 
#include <cstdlib> 
#include <time.h> 

int main(int argc, char** argv) 
{ 
    int accum = 0; 

    srand(time(NULL)); 

    for(unsigned int i = 0; i < 100000000; ++i) 
    { 
     accum += rand() % 365; 
    } 
    std::cout << accum << std::endl; 

    return 0; 
} 

Programma 2:

#include <iostream> 
#include <cmath> 
#include <cstdlib> 
#include <time.h> 

int main(int argc, char** argv) 
{ 

    float accum = 0; 
    srand(time(NULL)); 

    for(unsigned int i = 0; i < 100000000; ++i) 
    { 
     accum += (float)(rand() % 365); 
    } 
    std::cout << accum << std::endl; 

    return 0; 
} 

Grazie in anticipo!

Modifica: la piattaforma a cui tengo è regolare x86 o x86-64 in esecuzione su computer desktop Linux e Windows.

Modifica 2 (incollato da un commento di seguito): Attualmente disponiamo di una base di codice estesa. In realtà mi sono imbattuto nella generalizzazione secondo la quale "non dobbiamo usare float dato che il calcolo degli interi è più veloce" - e sto cercando un modo (se questo è vero) per smentire questa ipotesi generalizzata. Mi rendo conto che sarebbe impossibile prevedere il risultato esatto per noi, a corto di fare tutto il lavoro e di profilarlo in seguito.

In ogni caso, grazie per tutte le vostre eccellenti risposte e aiuto. Sentiti libero di aggiungere altro :).

+7

Quello che hai come test ora è banale. Probabilmente c'è anche una piccola differenza nell'assemblaggio, (ad esempio, 'addl' sostituito con' fadd', ad esempio). L'unico modo per ottenere una buona misura è ottenere una parte fondamentale del tuo programma reale e profilare diverse versioni di questo. Sfortunatamente questo può essere piuttosto difficile senza usare tonnellate di sforzi. Forse indicandoci l'hardware di destinazione e il tuo compilatore aiuterebbe le persone a fornirti un'esperienza preesistente, ecc. A proposito del tuo uso di interi, ho il sospetto che potresti creare una sorta di classe di template "fixed_point' che faciliterebbe enormemente tale lavoro. – GManNickG

+1

Ci sono ancora molte architetture là fuori che non hanno hardware dedicato in virgola mobile - alcuni tag che spiegano i sistemi che ti interessano ti aiuteranno a ottenere risposte migliori. –

+0

Questo è un buon punto. Al momento abbiamo una grande base di codice, e sto cercando di rendere l'argomento che sarebbe essenzialmente la stessa "velocità" in ogni caso.Sperando di trovare alcune prove a supporto del mio punto di vista - per giustificare il lavoro di passaggio. In ogni caso - grazie per l'idea del template - lo proverò. – maxpenguin

risposta

27

Ahimè, posso solo darvi un "dipende" risposta ...

Dalla mia esperienza, ci sono molte, molte variabili da prestazioni ... soprattutto tra interi & matematica in virgola mobile. Varia fortemente da processore a processore (anche all'interno della stessa famiglia come x86) perché i processori diversi hanno lunghezze "pipeline" differenti. Inoltre, alcune operazioni sono in genere molto semplici (come l'aggiunta) e hanno un percorso accelerato attraverso il processore, mentre altre (come la divisione) richiedono molto, molto più tempo.

L'altra grande variabile è dove risiedono i dati. Se hai solo pochi valori da aggiungere, tutti i dati possono risiedere nella cache, dove possono essere rapidamente inviati alla CPU. Un'operazione in virgola mobile molto, molto lenta che ha già i dati nella cache sarà molte volte più veloce di un'operazione intera in cui un intero deve essere copiato dalla memoria di sistema.

Suppongo che tu stia facendo questa domanda perché stai lavorando su un'applicazione performante. Se stai sviluppando per l'architettura x86 e hai bisogno di prestazioni extra, potresti voler utilizzare le estensioni SSE. Ciò può velocizzare enormemente l'aritmetica in virgola mobile a precisione singola, poiché la stessa operazione può essere eseguita su più dati contemporaneamente, inoltre vi è un banco * separato di registri per le operazioni SSE. (Ho notato che nel tuo secondo esempio hai usato "float" invece di "double", facendomi pensare che stai usando una matematica a precisione singola).

* Nota: l'utilizzo delle vecchie istruzioni MMX rallenterebbe effettivamente i programmi, poiché quelle vecchie istruzioni utilizzavano effettivamente gli stessi registri della FPU, rendendo impossibile l'uso simultaneo di FPU e MMX.

+7

E su alcuni processori la matematica FP può essere più veloce della matematica intera. Il processore Alpha aveva un'istruzione di divisione FP ma non un'intero, quindi la divisione intera doveva essere eseguita nel software. – Gabe

+0

Ottime informazioni qui, grazie. – maxpenguin

+0

SSEx velocizzerà anche l'aritmetica a doppia precisione? Mi dispiace, non ho molta familiarità con SSE –

3

A meno che non si stia scrivendo il codice che verrà chiamato milioni di volte al secondo (ad esempio, disegnando una linea sullo schermo in un'applicazione grafica), l'aritmetico intero vs virgola mobile è raramente il collo di bottiglia.

Il solito primo passo verso le domande sull'efficienza consiste nel profilare il codice per vedere dove viene effettivamente speso il tempo di esecuzione. Il comando linux per questo è gprof.

Edit:

Anche se suppongo si può sempre implementare l'algoritmo di disegno line utilizzando numeri interi e in virgola mobile i numeri, chiamare un gran numero di volte e vedere se fa la differenza:

http://en.wikipedia.org/wiki/Bresenham's_algorithm

+1

Le applicazioni scientifiche utilizzano FP. L'unico vantaggio di FP è che la precisione è invariante in scala. È come una notazione scientifica. Se si conosce già la scala dei numeri (ad esempio, che la lunghezza della linea è un numero di pixel), FP viene eliminato. Ma prima di arrivare a tracciare la linea, non è vero. – Potatoswatter

16

L'aggiunta è molto più veloce di rand, quindi il tuo programma è (soprattutto) inutile.

È necessario identificare gli hotspot delle prestazioni e modificare in modo incrementale il programma.Sembra che tu abbia problemi con il tuo ambiente di sviluppo che dovrà essere risolto prima. È impossibile eseguire il programma sul PC per un piccolo problema?

In genere, il tentativo di processi FP con aritmetica intera è una ricetta per lenta.

+0

Sì, così come la conversione da un intero rand a un float nella versione a virgola mobile. Qualche idea su un modo migliore per testarlo? – maxpenguin

+1

Se stai provando a velocizzare il profilo, guarda il 'timespec_t' di POSIX o qualcosa di simile. Registra l'ora all'inizio e alla fine del ciclo e fai la differenza. Quindi sposta la generazione dei dati 'rand' fuori dal ciclo. Assicurati che il tuo algoritmo recuperi tutti i suoi dati dagli array e metta tutti i suoi dati in array. Ciò ottiene da solo il tuo algoritmo e ottiene setup, malloc, stampa dei risultati, tutto tranne il cambio di attività e interruzioni dal ciclo di profilazione. –

+3

@maxpenguin: la domanda è ciò che stai testando. Artem ha ipotizzato che tu stia facendo grafica, Carl ha considerato se sei su una piattaforma embedded senza FP, suppongo che tu stia codificando la scienza per un server. Non è possibile generalizzare o "scrivere" benchmark. I benchmark sono campionati dal lavoro effettivo svolto dal tuo programma. Una cosa che posso dirti è che non rimarrà "essenzialmente la stessa velocità" se tocchi l'elemento critico delle prestazioni nel tuo programma, qualunque esso sia. – Potatoswatter

-1

Basato su quel "qualcosa che ho sentito" così affidabile, ai vecchi tempi, il calcolo di interi era da 20 a 50 volte più veloce di quello in virgola mobile, e in questi giorni è due volte più veloce.

+1

Si prega di considerare di guardare a questo di nuovo offrendo più di un'opinione (soprattutto dato che l'opinione sembra volare di fronte ai fatti raccolti) – MrMesees

+0

@MrMesees Mentre questa risposta non è terribilmente utile, direi che è coerente con i test che hai fatto. E probabilmente anche la curiosità storica va bene. –

3

Ho eseguito un test che ha appena aggiunto 1 al numero anziché a rand(). I risultati (su un x86-64) sono stati:

  • breve: 4.260s
  • int: 4.020s
  • lunga lunga: 3.350s
  • float: 7.330s
  • doppia: 7.210s
+1

Origine, opzioni di compilazione e metodo di sincronizzazione? Sono un po 'sorpreso dai risultati. – GManNickG

+0

Lo stesso ciclo di OP con "rand()% 365" sostituito da "1". Nessuna ottimizzazione. Tempo utente dal comando "tempo". – dan04

+11

"Nessuna ottimizzazione" è la chiave. Non si profila mai con l'ottimizzazione disattivata, sempre il profilo in modalità "rilascio". –

16

È probabile che vi sia una differenza significativa nella velocità del mondo reale tra la matematica a virgola fissa e quella a virgola mobile, ma il throughput teorico dei casi migliori tra ALU e FPU è completamente irrilevante. Invece, il numero di registri interi e virgola mobile (registri reali, non nomi dei registri) sulla tua architettura che non sono altrimenti utilizzati dal tuo calcolo (ad esempio per il controllo del ciclo), il numero di elementi di ogni tipo che si adattano a una linea della cache , ottimizzazioni possibili considerando la semantica diversa per la matematica a virgola mobile rispetto a quella a virgola mobile - questi effetti domineranno. Le dipendenze dei dati dell'algoritmo svolgono qui un ruolo significativo, in modo che nessun confronto generale preveda il divario di prestazioni sul tuo problema.Ad esempio, l'aggiunta di interi è commutativa, quindi se il compilatore vede un ciclo come quello che hai usato per un benchmark (supponendo che i dati casuali siano stati preparati in anticipo in modo da non oscurare i risultati), può srotolare il ciclo e calcolare somme parziali senza dipendenze, quindi aggiungerle quando termina il ciclo. Ma con il floating point, il compilatore deve eseguire le operazioni nello stesso ordine richiesto (ci sono dei punti di sequenza, quindi il compilatore deve garantire lo stesso risultato, il che non consente il riordino), quindi c'è una forte dipendenza di ogni aggiunta su il risultato del precedente.

È probabile che sia possibile montare più operandi interi nella cache alla volta. Quindi la versione a virgola fissa potrebbe sovraperformare la versione float di un ordine di grandezza anche su una macchina in cui la FPU ha un throughput teoricamente più elevato.

+0

Ottime informazioni anche. Grazie. – maxpenguin

+3

+1 per indicare come i benchmark ingenui possono produrre cicli 0 volte a causa di operazioni intere costanti su interi. Inoltre, il compilatore può scartare completamente il ciclo (intero o FP) se il risultato non è effettivamente utilizzato. – vladr

+0

La conclusione è questa: si deve chiamare una funzione che ha la variabile di ciclo come argomento. Dal momento che penso che nessun compilatore potrebbe essere in grado di vedere che la funzione non fa nulla e che la chiamata può essere ignorata. Poiché c'è un sovraccarico di chiamata, solo le differenze di tempo == (tempo di navigazione - tempo intero) saranno significative. – GameAlchemist

45

Per esempio (i numeri minori sono più veloci),

a 64 bit Intel Xeon X5550 @ 2.67GHz, gcc 4.1.2 -O3

short add/sub: 1.005460 [0] 
short mul/div: 3.926543 [0] 
long add/sub: 0.000000 [0] 
long mul/div: 7.378581 [0] 
long long add/sub: 0.000000 [0] 
long long mul/div: 7.378593 [0] 
float add/sub: 0.993583 [0] 
float mul/div: 1.821565 [0] 
double add/sub: 0.993884 [0] 
double mul/div: 1.988664 [0] 

32-bit Dual Core AMD Opteron (tm) Processor 265 @ 1.81GHz, gcc 3.4.6 -O3

short add/sub: 0.553863 [0] 
short mul/div: 12.509163 [0] 
long add/sub: 0.556912 [0] 
long mul/div: 12.748019 [0] 
long long add/sub: 5.298999 [0] 
long long mul/div: 20.461186 [0] 
float add/sub: 2.688253 [0] 
float mul/div: 4.683886 [0] 
double add/sub: 2.700834 [0] 
double mul/div: 4.646755 [0] 

Come Dan pointed out, anche una volta che si normalizzare per frequenza di clock (che può essere fuorviante in se stessa in disegni pipeline), risultati variano selvaggiamente basato su un'architettura CPU (individuale ALU/FPU prestazioni, nonché effettivo numero di ALU/FPU disponibile per core in superscalar disegni che influenza quante independent operations can execute in parallel - la quest'ultimo fattore non sia esercitata con il codice di seguito come tutte le operazioni seguenti si sequenzialmente dipendente)

FPU/ALU operazione benchmark povera, stile:.

#include <stdio.h> 
#ifdef _WIN32 
#include <sys/timeb.h> 
#else 
#include <sys/time.h> 
#endif 
#include <time.h> 

double 
mygettime(void) { 
# ifdef _WIN32 
    struct _timeb tb; 
    _ftime(&tb); 
    return (double)tb.time + (0.001 * (double)tb.millitm); 
# else 
    struct timeval tv; 
    if(gettimeofday(&tv, 0) < 0) { 
    perror("oops"); 
    } 
    return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec); 
# endif 
} 

template< typename Type > 
void my_test(const char* name) { 
    Type v = 0; 
    // Do not use constants or repeating values 
    // to avoid loop unroll optimizations. 
    // All values >0 to avoid division by 0 
    // Perform ten ops/iteration to reduce 
    // impact of ++i below on measurements 
    Type v0 = (Type)(rand() % 256)/16 + 1; 
    Type v1 = (Type)(rand() % 256)/16 + 1; 
    Type v2 = (Type)(rand() % 256)/16 + 1; 
    Type v3 = (Type)(rand() % 256)/16 + 1; 
    Type v4 = (Type)(rand() % 256)/16 + 1; 
    Type v5 = (Type)(rand() % 256)/16 + 1; 
    Type v6 = (Type)(rand() % 256)/16 + 1; 
    Type v7 = (Type)(rand() % 256)/16 + 1; 
    Type v8 = (Type)(rand() % 256)/16 + 1; 
    Type v9 = (Type)(rand() % 256)/16 + 1; 

    double t1 = mygettime(); 
    for (size_t i = 0; i < 100000000; ++i) { 
    v += v0; 
    v -= v1; 
    v += v2; 
    v -= v3; 
    v += v4; 
    v -= v5; 
    v += v6; 
    v -= v7; 
    v += v8; 
    v -= v9; 
    } 
    // Pretend we make use of v so compiler doesn't optimize out 
    // the loop completely 
    printf("%s add/sub: %f [%d]\n", name, mygettime() - t1, (int)v&1); 
    t1 = mygettime(); 
    for (size_t i = 0; i < 100000000; ++i) { 
    v /= v0; 
    v *= v1; 
    v /= v2; 
    v *= v3; 
    v /= v4; 
    v *= v5; 
    v /= v6; 
    v *= v7; 
    v /= v8; 
    v *= v9; 
    } 
    // Pretend we make use of v so compiler doesn't optimize out 
    // the loop completely 
    printf("%s mul/div: %f [%d]\n", name, mygettime() - t1, (int)v&1); 
} 

int main() { 
    my_test<short>("short"); 
    my_test<long>("long"); 
    my_test< long long >("long long"); 
    my_test<float>("float"); 
    my_test<double>("double"); 

    return 0; 
} 
+6

perché hai mescolato mult e div? Non dovrebbe essere interessante se mult è forse (o prevedibilmente?) Molto più veloce di div? –

+9

La moltiplicazione è molto più veloce della divisione in casi interi e in virgola mobile. Le prestazioni della divisione dipendono anche dalla dimensione dei numeri. Di solito presumo che la divisione sia ~ 15 volte più lenta. – Sogartar

+4

http://pastebin.com/Kx8WGUfg Ho preso il tuo benchmark e ho separato ogni operazione nel suo ciclo e ho aggiunto "volatile" per essere sicuro. Su Win64, FPU è inutilizzato e MSVC non genererà il codice per esso, quindi viene compilato utilizzando le istruzioni XMM e 'divss' XMM, che sono 25 volte più veloci della FPU in Win32. La macchina di prova è Core i5 M 520 @ 2,40 GHz –

7

Due punti da considerare -

L'hardware moderno può sovrapporsi alle istruzioni, eseguirle in parallelo e riordinarle per sfruttare al meglio l'hardware. Inoltre, qualsiasi programma in virgola mobile significativo ha probabilmente un lavoro intero significativo anche se calcola solo indici in matrici, contatori di loop ecc. Quindi anche se si dispone di un'istruzione a virgola mobile lenta potrebbe essere in esecuzione su un bit separato di hardware sovrapposto ad alcuni dei lavori interi. Il mio punto è che anche se le istruzioni in virgola mobile sono lente su quelle intere, il tuo programma generale potrebbe girare più veloce perché può utilizzare più hardware.

Come sempre, l'unico modo per essere sicuri è profilare il tuo programma attuale.

In secondo luogo, la maggior parte delle CPU in questi giorni dispone di istruzioni SIMD per virgola mobile in grado di operare su più valori in virgola mobile tutti allo stesso tempo. Ad esempio è possibile caricare 4 float in un singolo registro SSE e eseguire 4 moltiplicazioni su di essi tutti in parallelo. Se riesci a riscrivere parti del tuo codice per utilizzare le istruzioni SSE, sembra probabile che sarà più veloce di una versione intera. Visual C++ fornisce funzioni intrinseche del compilatore per fare ciò, vedere http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx per alcune informazioni.

+0

Si dovrebbe notare che su Win64, le istruzioni FPU non vengono più generate dal compilatore MSVC. Il punto mobile sta sempre usando le istruzioni SIMD lì. Questo crea una grande discrepanza di velocità tra Win32 e Win64 riguardo al flop. –

+2

Intero SIMD è altrettanto valido come SIMD a virgola mobile –

2

Oggi, le operazioni integer sono di solito un po 'più veloci delle operazioni in virgola mobile. Quindi, se puoi fare un calcolo con le stesse operazioni in interi e in virgola mobile, usa numeri interi.TUTTAVIA stai dicendo "Questo causa un sacco di fastidiosi problemi e aggiunge un sacco di codice fastidioso". Sembra che tu abbia bisogno di più operazioni perché usi l'aritmetica dei numeri interi invece del punto mobile. In tal caso, in virgola mobile verrà eseguito più velocemente perché

(a) non appena avete bisogno di più le operazioni di interi, probabilmente avete bisogno di molto di più, quindi il vantaggio leggero velocità è più che divorato dalle operazioni aggiuntive

(b) il codice in virgola mobile è più semplice, il che significa che è più veloce scrivere il codice, il che significa che se è critico per la velocità, è possibile dedicare più tempo all'ottimizzazione del codice.

+0

Qui c'è molta speculazione selvaggia, che non tiene conto di nessuno degli effetti secondari presenti nell'hardware, che spesso dominano il tempo di calcolo. Non è un brutto punto di partenza, ma deve essere controllato su ogni particolare applicazione tramite il profiling e non viene insegnato come gospel. –

3

La versione in virgola mobile sarà molto più lenta, se non vi è alcuna operazione di resto. Poiché tutti gli add sono sequenziali, la cpu non sarà in grado di parallelizzare la sommatoria. La latenza sarà critica. La latenza di aggiunta di FPU è in genere di 3 cicli, mentre l'aggiunta di interi è di 1 ciclo. Tuttavia, il divisore per l'operatore restante sarà probabilmente la parte critica, in quanto non è completamente pipeline su CPU moderne. quindi, supponendo che l'istruzione divide/resto consumi la maggior parte del tempo, la differenza dovuta all'aggiunta della latenza sarà piccola.