Calcoli in virgola mobile vs intero su hardware moderno

Sto facendo un po ‘di lavoro critico sulle prestazioni in C ++ e attualmente stiamo usando 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.

Ora, ricordo di aver letto su come i calcoli in virgola mobile siano stati così lenti approssimativamente intorno ai 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 è 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, non sono identici in alcun modo:

#include  #include  #include  #include  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  #include  #include  #include  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 qui sotto): Al momento disponiamo di un esteso numero di codice. 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 imansible prevedere il risultato esatto per noi, a corto di fare tutto il lavoro e profilarlo in seguito.

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

Ahimè, posso solo darti una risposta “dipende” …

Dalla mia esperienza, ci sono molte, molte variabili per le prestazioni … specialmente tra numeri interi e 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.

Presumo 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 usavano effettivamente gli stessi registri della FPU, rendendo imansible l’uso simultaneo di FPU e MMX.

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

Intel Xeon X5550 a 64 bit a 2,67 GHz, 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] 

Processore Dual Core AMD Opteron ™ a 32 bit 265 @ 1,81 GHz, gcc -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 ha sottolineato Dan , anche una volta normalizzata per la frequenza di clock (che può essere fuorviante in sé nei progetti pipeline), i risultati varieranno in modo selvaggio in base all’architettura della CPU ( prestazioni individuali ALU / FPU , oltre al numero effettivo di ALU / FPU disponibili per core in progetti superscalari che influenza il numero di operazioni indipendenti che possono essere eseguite in parallelo – quest’ultimo fattore non è esercitato dal codice seguente poiché tutte le operazioni sottostanti dipendono in modo sequenziale.)

Punto di riferimento dell’operazione FPU / ALU dei poveri:

 #include  #ifdef _WIN32 #include  #else #include  #endif #include  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; } 

È probabile che ci 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 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 con nessuna dipendenza, quindi aggiungerli 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 si adattino anche 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.

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. È imansible eseguire il programma sul PC per un piccolo problema?

In genere, tentare i processi FP con l’aritmetica intera è una ricetta per lenta.

TIL Questo varia (molto). Ecco alcuni risultati usando il compilatore gnu (btw ho anche controllato compilando su macchine, gnu g ++ 5.4 da xenial è molto più veloce di 4.6.3 da linaro su preciso)

Intel i7 4700MQ xenial

 short add: 0.822491 short sub: 0.832757 short mul: 1.007533 short div: 3.459642 long add: 0.824088 long sub: 0.867495 long mul: 1.017164 long div: 5.662498 long long add: 0.873705 long long sub: 0.873177 long long mul: 1.019648 long long div: 5.657374 float add: 1.137084 float sub: 1.140690 float mul: 1.410767 float div: 2.093982 double add: 1.139156 double sub: 1.146221 double mul: 1.405541 double div: 2.093173 

Intel i3 2370M ha risultati simili

 short add: 1.369983 short sub: 1.235122 short mul: 1.345993 short div: 4.198790 long add: 1.224552 long sub: 1.223314 long mul: 1.346309 long div: 7.275912 long long add: 1.235526 long long sub: 1.223865 long long mul: 1.346409 long long div: 7.271491 float add: 1.507352 float sub: 1.506573 float mul: 2.006751 float div: 2.762262 double add: 1.507561 double sub: 1.506817 double mul: 1.843164 double div: 2.877484 

Intel (R) Celeron (R) 2955U (Chromebook Acer C720 con xenial funzionante)

 short add: 1.999639 short sub: 1.919501 short mul: 2.292759 short div: 7.801453 long add: 1.987842 long sub: 1.933746 long mul: 2.292715 long div: 12.797286 long long add: 1.920429 long long sub: 1.987339 long long mul: 2.292952 long long div: 12.795385 float add: 2.580141 float sub: 2.579344 float mul: 3.152459 float div: 4.716983 double add: 2.579279 double sub: 2.579290 double mul: 3.152649 double div: 4.691226 

DigitalOcean 1GB Droplet CPU Intel (R) Xeon (R) E5-2630L v2 (affidabile)

 short add: 1.094323 short sub: 1.095886 short mul: 1.356369 short div: 4.256722 long add: 1.111328 long sub: 1.079420 long mul: 1.356105 long div: 7.422517 long long add: 1.057854 long long sub: 1.099414 long long mul: 1.368913 long long div: 7.424180 float add: 1.516550 float sub: 1.544005 float mul: 1.879592 float div: 2.798318 double add: 1.534624 double sub: 1.533405 double mul: 1.866442 double div: 2.777649 

Processore AMD Opteron ™ 4122 (preciso)

 short add: 3.396932 short sub: 3.530665 short mul: 3.524118 short div: 15.226630 long add: 3.522978 long sub: 3.439746 long mul: 5.051004 long div: 15.125845 long long add: 4.008773 long long sub: 4.138124 long long mul: 5.090263 long long div: 14.769520 float add: 6.357209 float sub: 6.393084 float mul: 6.303037 float div: 17.541792 double add: 6.415921 double sub: 6.342832 double mul: 6.321899 double div: 15.362536 

Questo utilizza il codice da http://pastebin.com/Kx8WGUfg come benchmark-pc.c

 g++ -fpermissive -O3 -o benchmark-pc benchmark-pc.c 

Ho eseguito più passaggi, ma sembra che i numeri generali siano gli stessi.

Un’eccezione degna di nota sembra essere ALU mul vs FPU mul. Addizione e sottrazione sembrano banalmente diverse.

Ecco quanto sopra in forma di grafico (clicca per ingrandire, più basso è più veloce e preferibile):

Grafico dei dati sopra

Aggiornamento per ospitare @Peter Cordes

https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc

i7 4700MQ Linux Ubuntu Xenial 64-bit (tutte le patch applicate a 2018-03-13)

  short add: 0.773049 short sub: 0.789793 short mul: 0.960152 short div: 3.273668 int add: 0.837695 int sub: 0.804066 int mul: 0.960840 int div: 3.281113 long add: 0.829946 long sub: 0.829168 long mul: 0.960717 long div: 5.363420 long long add: 0.828654 long long sub: 0.805897 long long mul: 0.964164 long long div: 5.359342 float add: 1.081649 float sub: 1.080351 float mul: 1.323401 float div: 1.984582 double add: 1.081079 double sub: 1.082572 double mul: 1.323857 double div: 1.968488 

Processore AMD Opteron ™ 4122 (preciso, DreamHost condiviso-hosting)

  short add: 1.235603 short sub: 1.235017 short mul: 1.280661 short div: 5.535520 int add: 1.233110 int sub: 1.232561 int mul: 1.280593 int div: 5.350998 long add: 1.281022 long sub: 1.251045 long mul: 1.834241 long div: 5.350325 long long add: 1.279738 long long sub: 1.249189 long long mul: 1.841852 long long div: 5.351960 float add: 2.307852 float sub: 2.305122 float mul: 2.298346 float div: 4.833562 double add: 2.305454 double sub: 2.307195 double mul: 2.302797 double div: 5.485736 

Intel Xeon E5-2630L v2 a 2,4 GHz (Trusty 64 bit, DigitalOcean VPS)

  short add: 1.040745 short sub: 0.998255 short mul: 1.240751 short div: 3.900671 int add: 1.054430 int sub: 1.000328 int mul: 1.250496 int div: 3.904415 long add: 0.995786 long sub: 1.021743 long mul: 1.335557 long div: 7.693886 long long add: 1.139643 long long sub: 1.103039 long long mul: 1.409939 long long div: 7.652080 float add: 1.572640 float sub: 1.532714 float mul: 1.864489 float div: 2.825330 double add: 1.535827 double sub: 1.535055 double mul: 1.881584 double div: 2.777245 

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.

Il secondo punto è che la maggior parte delle CPU in questi giorni hanno istruzioni SIMD per virgola mobile che possono operare su più valori in virgola mobile tutti allo stesso tempo. Ad esempio è ansible 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.

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

  • Insum: 4.260s
  • int: 4.020s
  • lungo lungo: 3,350 s
  • float: 7.330s
  • doppio: 7.210s

A meno che non si stia scrivendo un codice che verrà chiamato milioni di volte al secondo (ad esempio, disegnando una linea sullo schermo in un’applicazione grafica), l’aritmetica integer vs floating-point è 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 .

Modificare:

Anche se suppongo che tu possa sempre implementare l’algoritmo di disegno a linee usando numeri interi e numeri a virgola mobile, chiamalo un numero elevato di volte e vedi se fa la differenza:

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

La versione in virgola mobile sarà molto più lenta, se non ci sono operazioni di resto. Poiché tutti gli add sono sequenziali, la cpu non sarà in grado di parallelizzare la sumtoria. 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.

Oggi, le operazioni su interi 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, il punto mobile verrà eseguito più velocemente perché

  • non appena avrai bisogno di più operazioni intere, probabilmente avrai bisogno di molto altro, quindi il leggero vantaggio di velocità è più che consumato dalle operazioni aggiuntive

  • il codice a virgola mobile è più semplice, il che significa che è più veloce scrivere il codice, il che significa che se è critico per la velocità, è ansible dedicare più tempo all’ottimizzazione del codice.

Sulla base di quel “qualcosa che ho sentito” così affidabile, ai vecchi tempi, il calcolo dei numeri interi era da 20 a 50 volte più veloce di quello in virgola mobile, e in questi giorni è meno del doppio più veloce.