Quali codici operativi sono più veloci a livello di CPU? [chiuso]


19

In ogni linguaggio di programmazione ci sono serie di codici operativi raccomandati rispetto ad altri. Ho provato a elencarli qui, in ordine di velocità.

  1. bitwise
  2. Aggiunta / sottrazione di numeri interi
  3. Moltiplicazione / divisione di numeri interi
  4. Confronto
  5. Flusso di controllo
  6. Aggiunta / sottrazione del galleggiante
  7. Moltiplicazione / divisione del galleggiante

Laddove è necessario il codice ad alte prestazioni, C ++ può essere ottimizzato a mano in assembly, per utilizzare le istruzioni SIMD o un flusso di controllo più efficiente, tipi di dati, ecc. Quindi sto cercando di capire se il tipo di dati (int32 / float32 / float64) o l'operazione utilizzato ( *, +, &) influisce sulle prestazioni a livello di CPU.

  1. Un singolo multiplo è più lento sulla CPU di un'aggiunta?
  2. Nella teoria MCU apprendi che la velocità dei codici operativi è determinata dal numero di cicli CPU necessari per l'esecuzione. Quindi significa che moltiplicare richiede 4 cicli e aggiungere prende 2?
  3. Quali sono esattamente le caratteristiche di velocità dei codici operativi di flusso matematici e di controllo?
  4. Se due codici operativi richiedono lo stesso numero di cicli per essere eseguiti, entrambi possono essere utilizzati in modo intercambiabile senza alcun guadagno / perdita di prestazioni?
  5. Tutti gli altri dettagli tecnici che è possibile condividere per quanto riguarda le prestazioni della CPU x86 sono apprezzati

17
Questo suona molto come un'ottimizzazione prematura, e ricorda che il compilatore non produce ciò che scrivi, e non vuoi davvero scrivere assembly a meno che tu non ne abbia davvero.
Roy T.

3
La moltiplicazione e la divisione del float sono cose totalmente diverse, non dovresti metterle nella stessa categoria. Per i numeri n-bit, la moltiplicazione è un processo O (n) e la divisione è un processo O (nlogn). Ciò rende la divisione circa 5 volte più lenta della moltiplicazione su CPU moderne.
Sam Hocevar,

1
L'unica vera risposta è "profilalo".
Tetrad,

1
Espandendo la risposta di Roy, l'ottimizzazione manuale dell'assemblaggio sarà quasi sempre una perdita netta a meno che tu non sia davvero eccezionale. Le CPU moderne sono bestie molto complesse e buoni compilatori ottimizzati tirano fuori trasformazioni di codice che sono del tutto non ovvie e non banali da programmare a mano. Anche per SSE / SIMD, usa sempre i valori intrinseci in C / C ++ e lascia che il compilatore ottimizzi il loro utilizzo per te. L'uso dell'assemblaggio non elaborato disabilita le ottimizzazioni del compilatore e perdi molto.
Sean Middleditch l'

Non è necessario ottimizzare manualmente l'assemblaggio per utilizzare SIMD. SIMD è molto utile da ottimizzare a seconda della situazione, ma esiste una convenzione per lo più standard (funziona almeno su GCC e MSVC) per l'utilizzo di SSE2. Per quanto riguarda la tua lista, su un moderno processore multi-pipeline supserscalar, la dipendenza dai dati e la pressione del registro causano più problemi rispetto all'intero non elaborato e talvolta alle prestazioni in virgola mobile; lo stesso vale per la localizzazione dei dati. A proposito, la divisione intera è la stessa della moltiplicazione su un moderno x86
OrgnlDave

Risposte:


26

Le guide di ottimizzazione di Agner Fog sono eccellenti. Ha guide, tabelle dei tempi di istruzione e documenti sulla microarchitettura di tutti i recenti progetti di CPU x86 (risalenti fino a Intel Pentium). Vedi anche alcune altre risorse collegate da /programming//tags/x86/info

Solo per divertimento, risponderò ad alcune delle domande (numeri delle recenti CPU Intel). La scelta delle operazioni non è il fattore principale nell'ottimizzazione del codice (a meno che non sia possibile evitare la divisione).

Un singolo multiplo è più lento sulla CPU di un'aggiunta?

Sì (a meno che non sia per una potenza di 2). (3-4 volte la latenza, con una sola velocità effettiva per clock su Intel.) Non andare oltre il possibile per evitarlo, poiché è veloce come 2 o 3.

Quali sono esattamente le caratteristiche di velocità dei codici operativi di flusso matematici e di controllo?

Vedi le tabelle di istruzioni e la guida alla microarchitettura di Agner Fog se vuoi sapere esattamente : P. Fai attenzione ai salti condizionali. I salti incondizionati (come le chiamate di funzione) hanno un piccolo sovraccarico, ma non molto.

Se due codici operativi richiedono lo stesso numero di cicli per essere eseguiti, entrambi possono essere utilizzati in modo intercambiabile senza alcun guadagno / perdita di prestazioni?

No, potrebbero competere per la stessa porta di esecuzione di qualcos'altro, oppure no. Dipende da quali altre catene di dipendenze su cui la CPU può lavorare in parallelo. (In pratica, di solito non c'è alcuna decisione utile da prendere. Occasionalmente viene fuori che potresti usare uno spostamento vettoriale o uno shuffle vettoriale, che girano su porte diverse su CPU Intel. Ma spostamento per byte dell'intero registro ( PSLLDQecc.) funziona nell'unità shuffle.)

Tutti gli altri dettagli tecnici che è possibile condividere per quanto riguarda le prestazioni della CPU x86 sono apprezzati

I documenti microarch di Agner Fog descrivono le pipeline di CPU Intel e AMD in modo sufficientemente dettagliato per capire esattamente quanti cicli un ciclo dovrebbe richiedere per iterazione e se il collo di bottiglia è il throughput superiore, una catena di dipendenze o una contesa per una porta di esecuzione. Vedi alcune delle mie risposte su StackOverflow, come questa o questa .

Inoltre, http://www.realworldtech.com/haswell-cpu/ (e simili per i progetti precedenti) è divertente leggere se ti piace il design della CPU.

Ecco il tuo elenco, ordinato per una CPU Haswell, in base ai miei migliori ospiti. Questo non è davvero un modo utile di pensare alle cose per nient'altro che sintonizzare un asm loop. Gli effetti di previsione cache / branch in genere dominano, quindi scrivi il tuo codice per avere buoni schemi. I numeri sono molto ondulati a mano e cercano di tenere conto dell'alta latenza, anche se il throughput non è un problema, o per generare più uops che ostruiscono il tubo affinché altre cose possano accadere in parallelo. Esp. i numeri di cache / ramo sono molto inventati. La latenza è importante per le dipendenze portate in loop, il throughput è importante quando ogni iterazione è indipendente.

TL: DR questi numeri sono inventati in base a ciò che sto immaginando per un caso d'uso "tipico", per quanto riguarda i compromessi tra latenza, strozzature delle porte di esecuzione e throughput front-end (o bancarelle per cose come i fallimenti delle filiali ). Si prega di non utilizzare questi numeri per nessun tipo di analisi di perf serio .

  • Da 0,5 a 1 Bitwise / Aggiunta di interi / Sottrazione /
    spostamento e rotazione (conteggio const-tempo di compilazione) /
    versioni vettoriali di tutti questi (da 1 a 4 per ciclo di produzione, 1 ciclo di latenza)
  • 1 vettore min, max, confronta-uguale, confronta-maggiore (per creare una maschera)
  • 1.5 shuffle vettoriali. Haswell e quelli più recenti hanno solo una porta shuffle, e mi sembra che sia necessario un sacco di shuffle se ne hai bisogno, quindi lo sto ponderando leggermente più in alto per incoraggiare a pensare di usare meno shuffle. Non sono gratuiti, esp. se hai bisogno di una maschera di controllo pshufb dalla memoria.
  • 1.5 load / store (hit della cache L1. Throughput migliore della latenza)
  • Moltiplicazione di 1,75 numeri interi (latenza 3c / uno per 1c tput su Intel, 4c lat su AMD e solo uno per 2c tput). Le costanti piccole sono ancora più economiche usando LEA e / o ADD / SUB / shift . Ma ovviamente le costanti del tempo di compilazione sono sempre buone e spesso possono ottimizzare in altre cose. (E la moltiplicazione in un ciclo può spesso essere ridotta dal compilatore tmp += 7in un ciclo anziché in tmp = i*7)
  • 1,75 alcuni shuffle di vettore 256b (latenza extra su insn che può spostare i dati tra le corsie 128b di un vettore AVX). (O da 3 a 7 su Ryzen dove i riordini di corsia hanno bisogno di molti più salti)
  • 2 fp add / sub (e versioni vettoriali dello stesso) (1 o 2 per throughput del ciclo, latenza da 3 a 5 cicli). Può essere lento se si verificano colli di bottiglia in termini di latenza, ad esempio sommando un array con solo 1 sumvariabile. (Potrei pesare questo e fp mul a partire da 1 o fino a 5 a seconda del caso d'uso).
  • 2 vettoriali fp mul o FMA. (x * y + z è economico quanto un mul o un add se compilato con il supporto FMA abilitato).
  • 2 inserimento / estrazione di registri di uso generale in elementi vettoriali ( _mm_insert_epi8, ecc.)
  • 2.25 vector int mul (elementi a 16 bit o pmaddubsw che fa 8 * 8 -> 16 bit). Più economico su Skylake, con un rendimento migliore rispetto al mul scalare
  • 2,25 Maiusc / Ruota in base al conteggio variabile (latenza 2c, una velocità effettiva per 2c su Intel, più veloce su AMD o con BMI2)
  • 2.5 Confronto senza ramificazione ( y = x ? a : b, o y = x >= 0) ( test / setcco cmov)
  • 3 int-> conversione float
  • 3 Flusso di controllo perfettamente previsto (ramo previsto, chiamata, ritorno).
  • 4 vector int mul (elementi a 32 bit) (2 uops, 10c latenza su Haswell)
  • 4 divisione intera o %da una costante di compilazione (non potenza di 2).
  • 7 operazioni orizzontali vettoriali (ad es. PHADDAggiunta di valori all'interno di un vettore)
  • 11 (vector) FP Division (latenza 10-13c, una per throughput 7c o peggio). (Può essere economico se usato raramente, ma il throughput è da 6 a 40 volte peggiore di FP mul)
  • 13? Flusso di controllo (ramo scarsamente previsto, forse prevedibile al 75%)
  • 13 int division ( sì davvero , è più lento della divisione FP e non può essere vettoriale). (nota che i compilatori si dividono per una costante usando mul / shift / add con una costante magica e div / mod per potenze di 2 è molto economico.)
  • 16 (vettoriale) FP sqrt
  • 25? caricamento (hit cache L3). (i cache-miss store sono più economici dei carichi.)
  • 50? FP trig / exp / log. Se hai bisogno di molti exp / log e non hai bisogno della massima precisione, puoi scambiare l'accuratezza con la velocità con un polinomio più breve e / o una tabella. Puoi anche vettorializzare SIMD.
  • 50-80? ramo sempre riservato, che costa 15-20 cicli
  • 200-400? load / store (cache miss)
  • 3000 ??? leggere la pagina dal file (hit della cache del disco del sistema operativo) (inventando qui i numeri)
  • 20000 ??? pagina di lettura del disco (errore cache del disco del sistema operativo, SSD veloce) (numero totalmente inventato)

L'ho completamente inventato sulla base di congetture . Se qualcosa sembra sbagliato, è perché stavo pensando a un caso d'uso diverso o un errore di modifica.

Il costo relativo delle cose sulle CPU AMD sarà simile, tranne per il fatto che hanno shifter interi più veloci quando il conteggio dei turni è variabile. Le CPU della famiglia Bulldozer AMD sono ovviamente più lente sulla maggior parte del codice, per una serie di motivi. (Ryzen è abbastanza bravo in molte cose).

Tieni presente che è davvero impossibile ridurre le cose a un costo unidimensionale . A parte i mancati riscontri nella cache e gli errori di lettura delle diramazioni, il collo di bottiglia in un blocco di codice può essere latenza, throughput uop totale (frontend) o throughput di una porta specifica (porta di esecuzione).

Un'operazione "lenta" come la divisione FP può essere molto economica se il codice circostante mantiene occupata la CPU con altri lavori . (il vettore FP div o sqrt sono 1 uop ciascuno, hanno solo una latenza e una velocità effettiva cattive. Bloccano solo l'unità di divisione, non l'intera porta di esecuzione su cui si trova. Il div intero è di diverse uops.) Quindi se hai solo una divisione FP per ogni ~ 20 mul e aggiungere, e c'è altro lavoro che la CPU deve fare (ad esempio un'iterazione di loop indipendente), quindi il "costo" del div FP potrebbe essere più o meno lo stesso di un mul FP. Questo è probabilmente il miglior esempio di qualcosa che ha un basso rendimento quando è tutto ciò che stai facendo, ma si mescola molto bene con altri codici (quando la latenza non è un fattore), a causa dei bassi uops totali.

Nota che la divisione intera non è altrettanto amichevole con il codice circostante: su Haswell, è 9 uops, con una velocità effettiva di 8-11c e latenza di 22-29c. (La divisione a 64 bit è molto più lenta, anche su Skylake.) Quindi i numeri di latenza e velocità effettiva sono in qualche modo simili al div FP, ma il div FP è solo uno in alto.

Per esempi di analisi di una breve sequenza di insn per throughput, latenza e uops totali, vedere alcune delle mie risposte SO:

IDK se altre persone scrivono risposte SO, incluso questo tipo di analisi. Mi diverto molto più facilmente a trovare il mio, perché so di entrare spesso in questo dettaglio e posso ricordare quello che ho scritto.


Il "ramo previsto" a 4 ha senso - cosa dovrebbe essere realmente il "ramo previsto" a 20-25? (Avevo pensato che i rami sbagliati (elencati intorno ai 13) fossero molto più cari di così, ma è esattamente per questo che sono in questa pagina, per imparare qualcosa di più vicino alla verità - grazie per l'ottimo tavolo!)
Matt

@Matt: penso che si sia trattato di un errore di modifica e che si supponesse fosse un "ramo non previsto". Grazie per la segnalazione. Si noti che 13 è per un ramo previsto in modo imperfetto, non per un ramo sempre erroneamente previsto, quindi ho chiarito che. Ho rifatto la mano e fatto alcune modifiche. : P
Peter Cordes,

16

Dipende dalla CPU in questione, ma per una CPU moderna l'elenco è qualcosa del genere:

  1. Bit a bit, addizione, sottrazione, confronto, moltiplicazione
  2. Divisione
  3. Flusso di controllo (vedi risposta 3)

A seconda della CPU potrebbe esserci un costo considerevole per lavorare con tipi di dati a 64 bit.

Le tue domande:

  1. Niente affatto o in modo apprezzabile su una CPU moderna. Dipende dalla CPU.
  2. Quelle informazioni sono obsolete da 20 a 30 anni (la scuola fa schifo, ora hai le prove), le moderne CPU gestiscono un numero variabile di istruzioni per orologio, quante dipendono da ciò che lo scheduler produce.
  3. La divisione è un po 'più lenta delle altre, il flusso di controllo è molto veloce se la previsione del ramo è corretta e molto lenta se è sbagliata (qualcosa come 20 cicli, dipende dalla CPU). Il risultato è che molto codice è limitato principalmente dal flusso di controllo. Non fare ifciò che puoi ragionevolmente fare con l'aritmetica.
  4. Non esiste un numero fisso per quanti cicli richiede un'istruzione, ma a volte due istruzioni diverse possono funzionare allo stesso modo, inserendole in un altro contesto e forse non lo fanno, eseguendole su una CPU diversa e probabilmente vedrai un terzo risultato.
  5. Oltre al flusso di controllo, l'altro grande perditempo è la mancanza di cache, ogni volta che si tenta di leggere i dati che non si trovano nella cache, la CPU dovrà attendere che venga recuperata dalla memoria. In generale, dovresti provare a gestire i pezzi di dati uno accanto all'altro contemporaneamente piuttosto che raccogliere i dati da ogni luogo.

E infine, se stai realizzando un gioco, non preoccuparti troppo di tutto questo, concentrati meglio sul fare un buon gioco piuttosto che interrompere i cicli della CPU.


Vorrei anche sottolineare che l'FPU è piuttosto dannatamente veloce: specialmente su Intel, quindi il punto fisso è davvero necessario solo se si desidera risultati deterministici.
Jonathan Dickinson,

2
Metterei più enfasi sull'ultima parte: fare un buon gioco. Aiuta a chiarire il codice, motivo per cui 3. si applica solo quando si misura effettivamente un problema di prestazioni. È sempre facile cambiare quei se in qualcosa di meglio se se ne presenta la necessità. D'altra parte, 5. è più complicato - sono assolutamente d'accordo sul fatto che è un caso in cui si vuole davvero pensare prima, dal momento che di solito significa cambiare l'architettura.
Luaan,

3

Ho fatto un test sull'operazione di numeri interi in loop un milione di volte su x64_64, raggiungendo una breve conclusione come di seguito,

aggiungi --- 116 microsecondi

sotto ---- 116 microsecondi

mul ---- 1036 microsecondi

div ---- 13037 microsecondi

i dati di cui sopra hanno già ridotto il sovraccarico indotto da loop,


2

I manuali del processore Intel sono scaricabili gratuitamente dal loro sito Web. Sono abbastanza grandi ma tecnicamente possono rispondere alla tua domanda. Il manuale di ottimizzazione in particolare è quello che stai cercando, ma il manuale di istruzioni ha anche i tempi e le latenze per la maggior parte delle principali linee di CPU per le istruzioni simd poiché variano da chip a chip.

In generale, prenderei in considerazione i rami pieni e l'inseguimento dei puntatori (elenchi di collegamenti traverali, chiamate funzioni virtuali) in cima ai perf killer, ma i cpus x86 / x64 sono molto buoni in entrambi, rispetto ad altre architetture. Se porti mai su un'altra piattaforma vedrai quanto possono essere un problema, se stai scrivendo codice ad alte prestazioni.


+1, carichi dipendenti (inseguimento puntatore) è un grosso problema. Una mancata memorizzazione nella cache bloccherà anche i futuri carichi. Avere molti carichi dalla memoria principale in volo in una volta offre una larghezza di banda molto migliore rispetto ad avere un op richiede il completamento completo del precedente.
Peter Cordes,
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.