Calcoli in virgola mobile vs interi su hardware moderno


100

Sto eseguendo un lavoro critico per le prestazioni in C ++ e attualmente stiamo utilizzando calcoli interi per problemi che sono intrinsecamente in virgola mobile perché "è più veloce". Ciò causa molti fastidiosi problemi e aggiunge molto codice fastidioso.

Ora, ricordo di aver letto di come i calcoli in virgola mobile fossero così lenti all'incirca nei 386 giorni, in cui 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 di "velocità" fare calcoli in virgola mobile o interi? Soprattutto dal momento che il tempo di calcolo effettivo è minimo rispetto a qualcosa come causare un blocco della pipeline o recuperare qualcosa dalla memoria principale?

So che la risposta corretta è eseguire il benchmark sull'hardware di destinazione, quale sarebbe un buon modo per testarlo? Ho scritto due minuscoli programmi C ++ e ho confrontato il loro tempo di esecuzione con il "tempo" su Linux, ma il tempo di esecuzione effettivo è troppo variabile (non aiuta se sto eseguendo su un server virtuale). A parte passare la mia intera giornata eseguendo centinaia di benchmark, facendo grafici ecc., C'è qualcosa che posso fare per ottenere un test ragionevole della velocità relativa? Qualche idea o pensiero? Mi sbaglio completamente?

I programmi che ho usato come segue, non sono affatto identici:

#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 che mi interessa è la normale x86 o x86-64 in esecuzione su computer desktop Linux e Windows.

Modifica 2 (incollato da un commento di seguito): attualmente disponiamo di un'ampia base di codice. In realtà mi sono scontrato con la generalizzazione che "non dobbiamo usare il float poiché il calcolo dell'intero è più veloce" - e sto cercando un modo (se questo è anche vero) per confutare questa ipotesi generalizzata. Mi rendo conto che sarebbe impossibile prevedere il risultato esatto per noi se non fare tutto il lavoro e profilarlo in seguito.

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


8
Quello che hai come test ora è banale. Probabilmente c'è anche una piccola differenza nell'assemblaggio ( addlsostituito con fadd, ad esempio). L'unico modo per ottenere davvero una buona misurazione è ottenere una parte fondamentale del tuo programma reale e profilarne diverse versioni. Sfortunatamente può essere piuttosto difficile senza usare un sacco di sforzi. Forse dirci l'hardware di destinazione e il tuo compilatore aiuterebbe le persone a darti almeno un'esperienza preesistente, ecc. Riguardo al tuo uso intero, sospetto che potresti creare una sorta di fixed_pointclasse modello che faciliterebbe enormemente tale lavoro.
GManNickG

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

3
Credo che l'hardware del mio HTC Hero (Android) non abbia FPU, ma l'hardware di Google NexusOne (Android) sì. qual è il tuo obiettivo? pc desktop / server? netbook (possibile braccio + linux)? telefoni?
SteelBytes

5
Se vuoi FP veloce su x86, prova a compilare con l'ottimizzazione e la generazione di codice SSE. SSE (qualunque versione) può fare almeno addizione, sottrazione e moltiplicazione float in un singolo ciclo. Le funzioni Dividi, mod e superiori saranno sempre lente. Nota anche che floatottiene l'aumento di velocità, ma di solito doubleno.
Mike D.

1
Il numero intero a virgola fissa approssima FP utilizzando più operazioni di numero intero per evitare che i risultati traboccino. È quasi sempre più lento del semplice utilizzo delle FPU estremamente capaci che si trovano nelle moderne CPU desktop. es. MAD, il decodificatore mp3 a virgola fissa, è più lento di libmpg123, e anche se è di buona qualità per un decodificatore a virgola fissa, libmpg123 ha ancora meno errori di arrotondamento. wezm.net/technical/2008/04/mp3-decoder-libraries- a confronto per i benchmark su un PPC G5.
Peter Cordes

Risposte:


35

Ahimè, posso solo darti una risposta "dipende" ...

Dalla mia esperienza, ci sono molte, molte variabili per le prestazioni ... specialmente tra matematica intera e matematica in virgola mobile. Varia notevolmente da processore a processore (anche all'interno della stessa famiglia come x86) perché processori diversi hanno lunghezze di "pipeline" diverse. Inoltre, alcune operazioni sono generalmente 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 inviati rapidamente 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 con numeri interi in cui è necessario copiare un numero intero dalla memoria di sistema.

Presumo che tu stia ponendo questa domanda perché stai lavorando su un'applicazione critica per le prestazioni. Se stai sviluppando per l'architettura x86 e hai bisogno di prestazioni extra, potresti prendere in considerazione l'utilizzo delle estensioni SSE. Ciò può accelerare notevolmente l'aritmetica in virgola mobile a precisione singola, poiché la stessa operazione può essere eseguita su più dati contemporaneamente, in più c'è un banco separato * di registri per le operazioni SSE. (Ho notato nel tuo secondo esempio che hai usato "float" invece di "double", facendomi pensare che stai usando la matematica a precisione singola).

* Nota: l'uso delle vecchie istruzioni MMX rallentava effettivamente i programmi, perché quelle vecchie istruzioni utilizzavano effettivamente gli stessi registri dell'FPU, rendendo impossibile utilizzare contemporaneamente sia l'FPU che l'MMX.


8
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 numero intero, quindi la divisione dei numeri interi doveva essere eseguita nel software.
Gabe

SSEx accelererà anche l'aritmetica a doppia precisione? Mi dispiace, non ho molta familiarità con SSE
Johannes Schaub - litb

1
@ JohannesSchaub-litb: SSE2 (linea di base per x86-64) ha doubleFP imballato -precision. Con solo due 64 bit doubleper registro, il potenziale aumento di velocità è inferiore a quello floatdel codice che vettorializza bene. Scalare floate doubleusa i registri XMM su x86-64, con legacy x87 usato solo per long double. (Quindi @ Dan: no, i registri MMX non sono in conflitto con i normali registri FPU, perché il normale FPU su x86-64 è l'unità SSE. MMX sarebbe inutile perché se puoi fare un SIMD intero, vuoi 16 byte xmm0..15invece di 8 -byte mm0..7, e le CPU moderne hanno un throughput MMX peggiore di SSE.)
Peter Cordes

1
Ma le istruzioni intere MMX e SSE * / AVX2 competono per le stesse unità di esecuzione, quindi utilizzarle entrambe contemporaneamente non è quasi mai utile. Usa le versioni XMM / YMM più larghe per lavorare di più. L'uso di SIMD integer e FP allo stesso tempo compete per gli stessi registri, ma x86-64 ne ha 16. Ma i limiti di throughput totale significano che non è possibile ottenere il doppio del lavoro svolto utilizzando unità di esecuzione di interi e FP in parallelo.
Peter Cordes

49

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 AMD Opteron (tm) Dual Core a 32 bit 265 a 1,81 GHz, 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 ha sottolineato Dan , anche una volta normalizzata la frequenza di clock (che può essere fuorviante di per sé nei progetti pipeline), i risultati varieranno ampiamente in base all'architettura della CPU ( prestazioni singole ALU / FPU , così come il numero effettivo di ALU / FPU disponibili per core nei progetti superscalari che influenza il numero di operazioni indipendenti che possono essere eseguite in parallelo - quest'ultimo fattore non viene esercitato dal codice seguente poiché tutte le operazioni seguenti sono sequenzialmente dipendenti.)

Benchmark operativo FPU / ALU dei poveri:

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

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;
}

8
perché hai mescolato mult e div? Non dovrebbe essere interessante se mult è forse (o previsto?) Molto più veloce di div?
Kyss Tao

13
La moltiplicazione è molto più veloce della divisione sia nei casi interi che in quelli 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
pastebin.com/Kx8WGUfg Ho preso il tuo benchmark e ho separato ogni operazione nel suo ciclo e aggiunto volatileper essere sicuro. Su Win64, l'FPU è inutilizzato e MSVC non genererà il codice per esso, quindi compila usando mulsse le divssistruzioni XMM lì, che sono 25 volte più veloci dell'FPU in Win32. La macchina di prova è Core i5 M 520 a 2,40 GHz
James Dunne

4
@JamesDunne fai solo attenzione, perché le operazioni di fp vraggiungeranno rapidamente 0 o +/- inf molto rapidamente, il che può o non può essere (teoricamente) trattato come un caso speciale / fastpatheed da alcune implementazioni di fpu.
vladr

3
Questo "benchmark" non ha parallelismo dei dati per l'esecuzione fuori ordine, perché ogni operazione viene eseguita con lo stesso accumulator ( v). Sui recenti progetti Intel, la divisione non è affatto pipeline ( divss/ divpsha una latenza di 10-14 cicli e lo stesso throughput reciproco). mulsstuttavia è una latenza di 5 cicli, ma può emetterne uno ogni ciclo. (O due per ciclo su Haswell, poiché la porta 0 e la porta 1 hanno entrambe un moltiplicatore per FMA).
Peter Cordes

23

È probabile che ci sia una differenza significativa nella velocità del mondo reale tra matematica in virgola fissa e in virgola mobile, ma il rendimento teorico nel migliore dei casi di ALU vs FPU è completamente irrilevante. Invece, il numero di registri interi e in virgola mobile (registri reali, non nomi di registro) sulla tua architettura che non sono altrimenti utilizzati dal tuo calcolo (ad esempio per il controllo del ciclo), il numero di elementi di ciascun tipo che si adattano a una riga della cache , ottimizzazioni possibili considerando le diverse semantiche per matematica intera e in virgola mobile: questi effetti dominano. Le dipendenze dai dati del tuo algoritmo giocano un ruolo significativo qui, in modo che nessun confronto generale possa prevedere 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 le somme parziali con nessuna dipendenza, quindi aggiungerle quando il ciclo termina. Ma con la virgola mobile, il compilatore deve eseguire le operazioni nello stesso ordine che hai richiesto (hai punti di sequenza lì dentro quindi il compilatore deve garantire lo stesso risultato, il che non consente il riordino) quindi c'è una forte dipendenza di ogni aggiunta da il risultato del precedente.

È probabile che tu inserisca anche più operandi interi alla volta nella cache. 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.


4
+1 per evidenziare come i benchmark ingenui possono produrre cicli a tempo zero a causa di operazioni su interi costanti srotolati. Inoltre, il compilatore può eliminare completamente il ciclo (intero o FP) se il risultato non viene effettivamente utilizzato.
vladr

La conclusione è: si deve chiamare una funzione con 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 overhead di chiamata, solo le differenze di tempo == (float time - integer time) saranno significative.
GameAlchemist

@GameAlchemist: molti compilatori eliminano le chiamate a funzioni vuote, come effetto collaterale dell'inlining. Devi fare uno sforzo per impedirlo.
Ben Voigt

L'OP sembrava che stesse parlando dell'uso di numeri interi per cose in cui FP sarebbe stato più naturale, quindi ci sarebbe voluto più codice intero per ottenere lo stesso risultato del codice FP. In questo caso, usa solo FP. Ad esempio, su hardware con una FPU (ad esempio una CPU desktop), i decodificatori MP3 interi a virgola fissa sono più lenti (e leggermente più errori di arrotondamento) rispetto ai decodificatori a virgola mobile. Le implementazioni a virgola fissa dei codec esistono principalmente per funzionare su CPU ARM ridotte senza hardware FP, solo FP emulato lento.
Peter Cordes

un esempio per il primo punto: su x86-64 con AVX-512 ci sono solo 16 registri GP ma 32 registri zmm, quindi la matematica a virgola mobile scalare potrebbe essere più veloce
phuclv

18

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

È necessario identificare i punti caldi delle prestazioni e modificare in modo incrementale il programma. Sembra che tu abbia problemi con il tuo ambiente di sviluppo che dovranno essere risolti prima. È impossibile eseguire il programma sul PC per un piccolo insieme di problemi?

Generalmente, tentare lavori FP con aritmetica intera è una ricetta per slow.


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

1
Se stai cercando di profilare la velocità, guarda POSIX timespec_to qualcosa di simile. Registra il tempo all'inizio e alla fine del ciclo e prendi la differenza. Quindi sposta la randgenerazione dei dati fuori dal ciclo. Assicurati che il tuo algoritmo ottenga tutti i suoi dati dagli array e inserisca tutti i suoi dati negli array. Questo ottiene il tuo algoritmo effettivo da solo e ottiene configurazione, malloc, stampa dei risultati, tutto tranne il cambio di attività e interruzioni dal tuo ciclo di profilazione.
Mike D.

3
@maxpenguin: la domanda è cosa stai testando. Artem ha pensato 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 puoi generalizzare o "scrivere" benchmark. I benchmark sono campionati dal lavoro effettivo svolto dal programma. Una cosa che posso dirti è che non rimarrà "essenzialmente la stessa velocità" se tocchi l'elemento critico per le prestazioni nel tuo programma, qualunque esso sia.
Potatoswatter

buon punto e buona risposta. Al momento disponiamo di una vasta base di codici. In realtà mi sono scontrato con la generalizzazione che "non dobbiamo usare il float poiché il calcolo dell'intero è più veloce" - e sto cercando un modo (se questo è anche vero) per confutare questa ipotesi generalizzata. Mi rendo conto che sarebbe impossibile prevedere il risultato esatto per noi se non fare tutto il lavoro e profilarlo in seguito. Ad ogni modo, grazie per il tuo aiuto.
maxpenguin

18

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 precise)

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)

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 Droplet da 1 GB Intel (R) Xeon (R) CPU E5-2630L v2 (in esecuzione 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 (tm) 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 comebenchmark-pc.c

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

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

Una notevole eccezione sembra essere ALU mul vs FPU mul. L'addizione e la sottrazione sembrano banalmente diverse.

Ecco quanto sopra sotto forma di grafico (fare clic per la dimensione intera, inferiore è più veloce e preferibile):

Grafico dei dati di cui sopra

Aggiornamento per accogliere @Peter Cordes

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

i7 4700MQ Linux Ubuntu Xenial 64 bit (tutte le patch per 2018-03-13 applicate)
    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 (tm) 4122 (preciso, hosting condiviso DreamHost)
    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 (affidabile 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

gcc5 forse auto-vettorizza qualcosa che gcc4.6 non ha fatto? Sta benchmark-pcmisurando una combinazione di velocità effettiva e latenza? Sul tuo Haswell (i7 4700MQ), la moltiplicazione dei numeri interi è 1 per throughput di clock, 3 cicli di latenza, ma l'add / sub intero è 4 per throughput di clock, 1 ciclo di latenza ( agner.org/optimize ). Quindi presumibilmente c'è un sacco di loop overhead che diluisce quei numeri per add e mul per risultare così vicini (long add: 0.824088 vs. long mul: 1.017164). (L'impostazione predefinita di gcc è di non srotolare i cicli, eccetto per lo srotolamento completo di conteggi di iterazioni molto bassi).
Peter Cordes

E BTW, perché non prova int, solo shorte long? Su Linux x86-64, shortè a 16 bit (e quindi ha rallentamenti dei registri parziali in alcuni casi), mentre longe long longsono entrambi i tipi a 64 bit. (Forse è progettato per Windows dove x86-64 usa ancora 32 bit long? O forse è progettato per la modalità a 32 bit.) Su Linux, l'ABI x32 ha 32 bit longin modalità 64 bit , quindi se hai le librerie installate , utilizzare gcc -mx32per compilare per ILP32. O semplicemente usa -m32e guarda i longnumeri.
Peter Cordes

E dovresti davvero controllare se il tuo compilatore ha auto-vettorizzato qualcosa. es. usando addpssu registri xmm invece di addss, per fare 4 FP aggiunge in parallelo in un'istruzione che è veloce quanto scalare addss. (Utilizzare -march=nativeper consentire l'utilizzo di qualsiasi set di istruzioni supportato dalla CPU, non solo la baseline SSE2 per x86-64).
Peter Cordes

@cincodenada per favore lascia i grafici che mostrano i 15 completi sul lato in quanto sono illustrativi delle prestazioni.
MrMesees

@PeterCordes cercherò di guardare domani, grazie per la tua diligenza.
MrMesees

7

Due punti da considerare:

L'hardware moderno può sovrapporre le istruzioni, eseguirle in parallelo e riordinarle per utilizzare al meglio l'hardware. Inoltre, è probabile che qualsiasi programma in virgola mobile significativo abbia anche un lavoro con interi significativi anche se calcola solo indici in array, contatore di loop ecc. Quindi anche se hai un'istruzione in virgola mobile lenta potrebbe essere in esecuzione su un bit separato di hardware sovrapposti con alcuni dei lavori interi. Il punto è che anche se le istruzioni in virgola mobile sono lente rispetto a quelle intere, il programma complessivo potrebbe essere eseguito più velocemente 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 ha istruzioni SIMD per virgola mobile che possono operare su più valori in virgola mobile contemporaneamente. Ad esempio è possibile caricare 4 float in un singolo registro SSE ed eseguire 4 moltiplicazioni su di essi tutte in parallelo. Se puoi 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 eseguire questa operazione, vedere http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx per alcune informazioni.


Si noti che su Win64, le istruzioni FPU non vengono più generate dal compilatore MSVC. Il virgola mobile utilizza sempre le istruzioni SIMD lì. Ciò crea una grande discrepanza di velocità tra Win32 e Win64 per quanto riguarda i flop.
James Dunne

5

La versione in virgola mobile sarà molto più lenta, se non ci sono operazioni con il resto. Poiché tutte le aggiunte sono sequenziali, la cpu non sarà in grado di parallelizzare la somma. La latenza sarà critica. La latenza di aggiunta della FPU è in genere di 3 cicli, mentre l'aggiunta di interi è di 1 ciclo. Tuttavia, il divisore per il resto dell'operatore sarà probabilmente la parte critica, in quanto non è completamente pipeline sulle moderne CPU. quindi, supponendo che l'istruzione di divisione / resto consumerà la maggior parte del tempo, la differenza dovuta alla latenza aggiunta sarà piccola.


4

A meno che tu non stia scrivendo codice che verrà chiamato milioni di volte al secondo (come, ad esempio, disegnare una linea sullo schermo in un'applicazione grafica), l'aritmetica di numeri interi e virgola mobile è raramente il collo di bottiglia.

Il solito primo passo per le domande sull'efficienza è profilare il codice per vedere dove viene realmente trascorso il tempo di esecuzione. Il comando linux per questo è gprof.

Modificare:

Anche se suppongo che tu possa sempre implementare l'algoritmo di disegno della linea usando numeri interi e numeri in virgola mobile, chiamalo un gran numero di volte e vedi se fa la differenza:

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


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

4

Oggi, le operazioni su interi sono generalmente un po 'più veloci delle operazioni in virgola mobile. Quindi, se puoi eseguire un calcolo con le stesse operazioni in numero intero e virgola mobile, usa il numero intero. TUTTAVIA stai dicendo "Questo causa molti fastidiosi problemi e aggiunge molto codice fastidioso". Sembra che tu abbia bisogno di più operazioni perché usi l'aritmetica dei numeri interi invece del virgola mobile. In tal caso, la virgola mobile verrà eseguita più velocemente perché

  • non appena avrai bisogno di più operazioni intere, probabilmente avrai bisogno di molte di più, 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à, puoi dedicare più tempo all'ottimizzazione del codice.


Ci sono molte speculazioni selvagge qui, che non tengono conto di nessuno degli effetti secondari presenti nell'hardware, che spesso dominano il tempo di calcolo. Non è un cattivo punto di partenza, ma deve essere verificato su ogni particolare applicazione tramite la creazione di profili e non deve essere insegnato come vangelo.
Ben Voigt

3

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

  • breve: 4.260s
  • int: 4.020 s
  • lungo lungo: 3.350 s
  • galleggiante: 7.330s
  • doppio: 7.210s

1
Fonte, opzioni di compilazione e metodo di temporizzazione? Sono un po 'sorpreso dai risultati.
GManNickG

Stesso ciclo di OP con "rand ()% 365" sostituito da "1". Nessuna ottimizzazione. Ora utente dal comando "time".
dan04

13
"Nessuna ottimizzazione" è la chiave. Non si profila mai con l'ottimizzazione disattivata, si profila sempre in modalità "rilascio".
Dean Harding

2
In questo caso, tuttavia, l'ottimizzazione disattivata costringe l'operazione a verificarsi e viene eseguita deliberatamente: il ciclo è lì per dilatare il tempo a una scala di misurazione ragionevole. L'uso della costante 1 rimuove il costo di rand (). Un compilatore di ottimizzazione sufficientemente intelligente vedrebbe 1 aggiunto 100.000.000 di volte senza via d'uscita dal ciclo e aggiungerebbe semplicemente 100000000 in una singola operazione. Quel tipo aggira l'intero scopo, non è vero?
Stan Rogers

7
@Stan, rendi la variabile volatile. Anche un compilatore ottimizzato intelligente dovrebbe quindi onorare le operazioni multiple.
vladr

0

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 quella virgola mobile, e oggigiorno è meno del doppio più veloce.


1
Si prega di considerare di guardare di nuovo questo offrendo più dell'opinione (soprattutto considerando che l'opinione sembra volare di fronte ai fatti raccolti)
MrMesees

1
@MrMesees Anche se questa risposta non è molto utile, direi che è coerente con i test che hai fatto. E probabilmente anche la curiosità storica va bene.
Jonatan Öström

Come qualcuno che ha lavorato con 286 nel corso della giornata, posso confermare; "Si lo erano!"
David H Parry
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.