Questo mi ha lasciato chiedendo quanto sia importante il multithreading nell'attuale scenario industriale?
Nei campi critici per le prestazioni in cui le prestazioni non provengono da codice di terze parti che esegue operazioni pesanti, ma le nostre, tenderei a considerare le cose in questo ordine di importanza dal punto di vista della CPU (la GPU è un jolly che ho vinto non entrare):
- Efficienza della memoria (es: località di riferimento).
- Algorithmic
- multithreading
- SIMD
- Altre ottimizzazioni (suggerimenti per la previsione di rami statici, ad es.)
Si noti che questo elenco non si basa solo sull'importanza, ma su molte altre dinamiche come l'impatto che hanno sulla manutenzione, quanto sono semplici (se no, vale la pena considerare più in anticipo), le loro interazioni con gli altri nell'elenco, ecc.
Efficienza di memoria
Molti potrebbero essere sorpresi dalla mia scelta di efficienza della memoria rispetto all'algoritmo. È perché l'efficienza della memoria interagisce con tutti e 4 gli altri elementi di questo elenco, ed è perché la sua considerazione è spesso molto nella categoria "design" piuttosto che nella categoria "implementazione". C'è sicuramente un po 'di pollo o il problema delle uova qui poiché la comprensione dell'efficienza della memoria richiede spesso di considerare tutti e 4 gli elementi dell'elenco, mentre anche tutti gli altri 4 elementi richiedono di considerare l'efficienza della memoria. Eppure è al centro di tutto.
Ad esempio, se abbiamo bisogno di una struttura di dati che offra un accesso sequenziale in tempo lineare e inserimenti in tempo costante sul retro e nient'altro per piccoli elementi, la scelta ingenua qui da raggiungere sarebbe un elenco collegato. Questo trascura l'efficienza della memoria. Quando consideriamo l'efficienza della memoria nel mix, finiamo per scegliere strutture più contigue in questo scenario, come strutture basate su array coltivabili o nodi più contigui (es: uno che memorizza 128 elementi in un nodo) collegati tra loro, o almeno un elenco collegato supportato da un allocatore di pool. Questi hanno un vantaggio drammatico nonostante abbiano la stessa complessità algoritmica. Allo stesso modo, spesso scegliamo quicksort di un array piuttosto che unire l'ordinamento nonostante una complessità algoritmica inferiore semplicemente a causa dell'efficienza della memoria.
Allo stesso modo, non possiamo avere un multithreading efficiente se i nostri modelli di accesso alla memoria sono così granulari e sparsi in natura che finiamo per massimizzare la quantità di false condivisioni bloccandoci ai livelli più granulari nel codice. Quindi l'efficienza della memoria moltiplica l'efficienza del multithreading. È un prerequisito per ottenere il massimo dai thread.
Ogni singolo elemento sopra nell'elenco ha un'interazione complessa con i dati e concentrarsi sul modo in cui i dati sono rappresentati è in definitiva sulla scia dell'efficienza della memoria. Ognuno di questi sopra può essere strozzato con un modo inappropriato di rappresentare o accedere ai dati.
Un altro motivo per cui l'efficienza della memoria è così importante è che può applicarsi in un'intera base di codice. Generalmente quando le persone immaginano che le inefficienze si accumulino da piccole porzioni di lavoro qua e là, è un segno che devono prendere un profiler. Tuttavia, i campi a bassa latenza o quelli che si occupano di hardware molto limitato troveranno effettivamente, anche dopo il profiling, sessioni che indicano nessun hotspot chiaro (solo volte disperse ovunque) in una base di codice che è palesemente inefficiente con il modo in cui viene allocato, copiato e accesso alla memoria. In genere si tratta dell'unica volta in cui un'intera base di codice può essere suscettibile a un problema di prestazioni che potrebbe portare a una serie completamente nuova di standard applicati in tutta la base di codice e l'efficienza della memoria è spesso al centro di essa.
Algorithmic
Questo è praticamente un dato di fatto, poiché la scelta in un algoritmo di ordinamento può fare la differenza tra un input massiccio che impiega mesi per ordinare rispetto a secondi per ordinare. Ha il maggiore impatto di tutti se la scelta è tra, diciamo, algoritmi quadratici o cubici davvero sotto-par e uno lineare-matematico, o tra lineare e logaritmico o costante, almeno fino a quando non avremo circa 1.000.000 di macchine core (nel qual caso la memoria l'efficienza diventerebbe ancora più importante).
Non è in cima alla mia lista personale, tuttavia, poiché chiunque sia competente nel loro campo saprebbe usare una struttura di accelerazione per l'abbattimento del frustum, ad esempio siamo saturi di conoscenza algoritmica e conoscendo cose come l'uso di una variante di un trie come un albero radicale per ricerche basate su prefisso è roba da bambini. Mancando questo tipo di conoscenza di base del campo in cui stiamo lavorando, l'efficienza algoritmica salirà sicuramente al vertice, ma spesso l'efficienza algoritmica è banale.
Anche inventare nuovi algoritmi può essere una necessità in alcuni campi (es: nell'elaborazione della mesh ho dovuto inventarne centinaia poiché non esistevano prima o le implementazioni di caratteristiche simili in altri prodotti erano segreti proprietari, non pubblicati in un documento ). Tuttavia, una volta superata la parte di risoluzione dei problemi e trovato un modo per ottenere i risultati corretti, e una volta che l'efficienza diventa l'obiettivo, l'unico modo per ottenerlo è considerare come interagiamo con i dati (memoria). Senza comprendere l'efficienza della memoria, il nuovo algoritmo può diventare inutilmente complesso con inutili sforzi per renderlo più veloce, quando l'unica cosa di cui aveva bisogno era una piccola considerazione dell'efficienza della memoria per produrre un algoritmo più semplice ed elegante.
Infine, gli algoritmi tendono ad essere più nella categoria "implementazione" che nell'efficienza della memoria. Spesso sono più facili da migliorare con il senno di poi anche con un algoritmo subottimale utilizzato inizialmente. Ad esempio, un algoritmo di elaborazione delle immagini inferiore viene spesso implementato in una posizione locale nella base di codice. Può essere sostituito con uno migliore in seguito. Tuttavia, se tutti gli algoritmi di elaborazione delle immagini sono collegati a Pixel
un'interfaccia con una rappresentazione della memoria non ottimale, ma l'unico modo per correggerlo è cambiare il modo in cui sono rappresentati più pixel (e non uno singolo), allora siamo spesso SOL e dovrà riscrivere completamente la base di codice verso unImage
interfaccia. Lo stesso tipo di cose vale per la sostituzione di un algoritmo di ordinamento: di solito è un dettaglio di implementazione, mentre una modifica completa alla rappresentazione sottostante dei dati ordinati o il modo in cui vengono passati attraverso i messaggi potrebbe richiedere una riprogettazione delle interfacce.
multithreading
Il multithreading è difficile nel contesto delle prestazioni poiché è un'ottimizzazione a livello micro che gioca sulle caratteristiche hardware, ma il nostro hardware si sta davvero ridimensionando in quella direzione. Ho già colleghi che hanno 32 core (ne ho solo 4).
Tuttavia, il mulithreading è tra le microottimizzazioni più pericolose probabilmente conosciute da un professionista se lo scopo viene utilizzato per accelerare il software. La condizione di gara è praticamente il bug più mortale possibile, dal momento che è di natura così indeterministica (forse apparire solo una volta ogni pochi mesi sulla macchina di uno sviluppatore in un momento molto scomodo al di fuori di un contesto di debug, se non del tutto). Quindi ha probabilmente il peggioramento più negativo sulla manutenibilità e la potenziale correttezza del codice tra tutti questi, soprattutto perché i bug relativi al multithreading possono facilmente volare sotto il radar anche dei test più accurati.
Tuttavia, sta diventando così importante. Anche se può non sempre superare qualcosa come l'efficienza della memoria (che a volte può rendere le cose cento volte più veloci) dato il numero di core che abbiamo ora, stiamo vedendo sempre più core. Naturalmente, anche con macchine a 100 core, metterei comunque l'efficienza della memoria in cima all'elenco, poiché l'efficienza del thread è generalmente impossibile senza di essa. Un programma può usare un centinaio di thread su una macchina del genere ed essere ancora lento senza un'efficiente rappresentazione della memoria e schemi di accesso (che si collegheranno a schemi di blocco).
SIMD
Il SIMD è anche un po 'imbarazzante poiché i registri si stanno effettivamente allargando, con piani di ampliamento. Inizialmente abbiamo visto i registri MMX a 64 bit seguiti dai registri XMM a 128 bit in grado di eseguire 4 operazioni SPFP in parallelo. Ora vediamo registri YMM a 256 bit in grado di 8 in parallelo. E ci sono già piani per registri a 512 bit che consentirebbero 16 in parallelo.
Questi interagirebbero e si moltiplicherebbero con l'efficienza del multithreading. Tuttavia, SIMD può degradare la manutenibilità tanto quanto il multithreading. Anche se i bug ad essi correlati non sono necessariamente difficili da riprodurre e correggere come un deadlock o una condizione di competizione, la portabilità è scomoda e garantire che il codice possa essere eseguito sulla macchina di tutti (e utilizzando le istruzioni appropriate basate sulle loro capacità hardware) è imbarazzante.
Un'altra cosa è che mentre i compilatori oggi di solito non battono il codice SIMD scritto da esperti, battono facilmente ingenui tentativi. Potrebbero migliorare al punto in cui non dovremo più farlo manualmente, o almeno senza essere così manuali da scrivere intrinseci o codice di assemblaggio diretto (forse solo una piccola guida umana).
Ancora una volta, però, senza un layout di memoria efficiente per l'elaborazione vettoriale, SIMD è inutile. Finiremo semplicemente caricando un campo scalare in un registro largo solo per fare un'operazione su di esso. Al centro di tutti questi elementi c'è una dipendenza dai layout di memoria per essere veramente efficiente.
Altre ottimizzazioni
Questi sono spesso ciò che suggerirei di iniziare a chiamare "micro" al giorno d'oggi se la parola suggerisce non solo di andare oltre il focus algoritmico ma verso cambiamenti che hanno un impatto minuscolo sulle prestazioni.
Spesso cercare di ottimizzare la previsione del ramo richiede una modifica dell'algoritmo o dell'efficienza della memoria, ad esempio se questo viene tentato semplicemente attraverso suggerimenti e riorganizzare il codice per la previsione statica, ciò tende solo a migliorare l'esecuzione per la prima volta di tale codice, rendendo gli effetti discutibili se spesso non del tutto trascurabile.
Torna al multithreading per prestazioni
Quindi, quanto è importante il multithreading da un contesto di performance? Sulla mia macchina a 4 core, può idealmente rendere le cose circa 5 volte più veloci (cosa posso ottenere con l'hyperthreading). Sarebbe molto più importante per il mio collega che ha 32 core. E diventerà sempre più importante negli anni a venire.
Quindi è abbastanza importante. Ma è inutile lanciare un sacco di thread al problema se l'efficienza della memoria non è lì per consentire ai blocchi di essere usati con parsimonia, per ridurre la falsa condivisione, ecc.
Multithreading al di fuori delle prestazioni
Il multithreading non riguarda sempre le prestazioni allo stato puro in un senso del throughput diretto. A volte viene utilizzato per bilanciare un carico anche al possibile costo del throughput per migliorare la reattività per l'utente o per consentire all'utente di eseguire più operazioni multitasking senza attendere il completamento delle operazioni (ad esempio: continuare la navigazione durante il download di un file).
In quei casi, suggerirei che il multithreading sale ancora più in alto (forse anche al di sopra dell'efficienza della memoria), dal momento che si tratta quindi di progettazione da parte dell'utente piuttosto che di ottenere il massimo dall'hardware. Spesso dominerà i progetti di interfaccia e il modo in cui strutturiamo la nostra intera base di codice in tali scenari.
Quando non stiamo semplicemente parallelizzando un circuito ristretto che accede a un'enorme struttura di dati, il multithreading passa alla categoria del "design" davvero hardcore e il design prevale sempre sull'implementazione.
Quindi, in quei casi, direi che considerare il multithreading in anticipo è assolutamente critico, anche più della rappresentazione e dell'accesso alla memoria.