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 ( PSLLDQ
ecc.) 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 += 7
in 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
sum
variabile. (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 / setcc
o 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.
PHADD
Aggiunta 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.