Cosa significano i termini "CPU bound" e "I / O bound"?
Cosa significano i termini "CPU bound" e "I / O bound"?
Risposte:
È abbastanza intuitivo:
Un programma è associato alla CPU se andrebbe più veloce se la CPU fosse più veloce, ovvero trascorre la maggior parte del suo tempo semplicemente usando la CPU (facendo calcoli). Un programma che calcola nuove cifre di π sarà tipicamente associato alla CPU, sta solo sgretolando i numeri.
Un programma è associato a I / O se andrebbe più veloce se il sottosistema I / O fosse più veloce. Quale sistema I / O esatto si intende può variare; In genere lo associo al disco, ma ovviamente anche la rete o la comunicazione in generale sono comuni. Un programma che cerca un enorme file per alcuni dati potrebbe diventare associato I / O, poiché il collo di bottiglia è quindi la lettura dei dati dal disco (in realtà, questo esempio è forse un po 'vecchio stile in questi giorni con centinaia di MB / s proveniente da SSD).
CPU Bound indica che la velocità con cui il processo avanza è limitata dalla velocità della CPU. Un'attività che esegue calcoli su una piccola serie di numeri, ad esempio moltiplicando piccole matrici, è probabilmente associata alla CPU.
I / O Bound indica la velocità con cui un processo avanza è limitata dalla velocità del sottosistema I / O. Un'attività che elabora i dati dal disco, ad esempio, contando il numero di righe in un file è probabilmente associata a I / O.
Limite di memoria indica la velocità con cui un processo avanza è limitata dalla quantità di memoria disponibile e dalla velocità di accesso alla memoria. Un'attività che elabora grandi quantità di dati in memoria, ad esempio moltiplicando matrici di grandi dimensioni, è probabilmente legata alla memoria.
Limite cache indica la velocità con cui l'avanzamento di un processo è limitato dalla quantità e dalla velocità della cache disponibile. Un'attività che elabora semplicemente più dati di quelli inseriti nella cache sarà associata alla cache.
L'I / O Bound sarebbe più lento di Memory Bound sarebbe più lento di Cache Bound sarebbe più lento di CPU Bound.
La soluzione per essere associato I / O non è necessariamente quella di ottenere più memoria. In alcune situazioni, l'algoritmo di accesso potrebbe essere progettato attorno alle limitazioni di I / O, memoria o cache. Vedi Cache Oblivious Algorithms .
Multi-threading
In questa risposta, esaminerò un importante caso d'uso di distinzione tra lavoro con CPU e IO: quando scrivo codice multi-thread.
Esempio di associazione I / O RAM: Vector Sum
Considera un programma che somma tutti i valori di un singolo vettore:
#define SIZE 1000000000
unsigned int is[SIZE];
unsigned int sum = 0;
size_t i = 0;
for (i = 0; i < SIZE; i++)
/* Each one of those requires a RAM access! */
sum += is[i]
Parallelizzare che suddividere equamente l'array per ciascuno dei core è di utilità limitata sui desktop moderni comuni.
Ad esempio, sul mio Ubuntu 19.04, laptop Lenovo ThinkPad P51 con CPU: CPU Intel Core i7-7820HQ (4 core / 8 thread), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB) Ottengo risultati come questo:
Si noti che esiste comunque una grande varianza tra l'esecuzione. Ma non riesco ad aumentare ulteriormente le dimensioni dell'array poiché sono già a 8GiB e oggi non sono in vena di statistiche su più esecuzioni. Sembrava comunque una corsa tipica dopo aver fatto molte corse manuali.
Codice benchmark:
pthread
Codice sorgente POSIX C utilizzato nel grafico.
Ed ecco una versione C ++ che produce risultati analoghi.
Non conosco abbastanza architettura del computer per spiegare appieno la forma della curva, ma una cosa è chiara: il calcolo non diventa 8 volte più veloce come ingenuamente previsto a causa mia usando tutti i miei 8 thread! Per qualche motivo, 2 e 3 thread sono stati ottimali e l'aggiunta di più rende le cose molto più lente.
Confronta questo con il lavoro associato alla CPU, che in realtà ottiene 8 volte più veloce: cosa significano "reale", "utente" e "sys" nell'output di time (1)?
Il motivo è che tutti i processori condividono un singolo bus di memoria collegato alla RAM:
CPU 1 --\ Bus +-----+
CPU 2 ---\__________| RAM |
... ---/ +-----+
CPU N --/
quindi il bus di memoria diventa rapidamente il collo di bottiglia, non la CPU.
Ciò accade perché l'aggiunta di due numeri richiede un singolo ciclo della CPU, le letture della memoria richiedono circa 100 cicli della CPU nell'hardware del 2016.
Quindi il lavoro della CPU svolto per byte di dati di input è troppo piccolo e lo chiamiamo un processo IO-bound.
L'unico modo per accelerare ulteriormente tale calcolo sarebbe quello di accelerare gli accessi alla memoria individuale con nuovo hardware di memoria, ad esempio memoria multicanale .
Ad esempio, l'aggiornamento a un clock della CPU più veloce non sarebbe molto utile.
Altri esempi
la moltiplicazione della matrice è legata alla CPU su RAM e GPU. L'input contiene:
2 * N**2
numeri, ma:
N ** 3
vengono fatte le moltiplicazioni, e questo è sufficiente affinché la parallelizzazione valga la pena per la pratica N. grande
Ecco perché esistono librerie di moltiplicazione di matrici di CPU parallele come le seguenti:
L'utilizzo della cache fa una grande differenza per la velocità delle implementazioni. Vedi ad esempio questo esempio di confronto della GPU didattica .
Guarda anche:
Il networking è l'esempio prototipo di IO-bound.
Anche quando inviamo un singolo byte di dati, ci vuole ancora molto tempo per raggiungere la sua destinazione.
Parallelizzare le richieste di piccole reti come le richieste HTTP può offrire enormi vantaggi in termini di prestazioni.
Se la rete è già a piena capacità (ad esempio il download di un torrent), la parallelizzazione può comunque aumentare migliorare la latenza (ad esempio è possibile caricare una pagina Web "contemporaneamente").
Un'operazione fittizia legata alla CPU C ++ che richiede un numero e lo scricchiola molto:
L'ordinamento sembra essere basato sulla CPU in base al seguente esperimento: gli algoritmi paralleli C ++ 17 sono già implementati? che ha mostrato un miglioramento delle prestazioni 4x per l'ordinamento parallelo, ma vorrei avere anche una conferma più teorica
Come scoprire se si è collegati alla CPU o all'IO
IO non RAM associato come disco, rete ps aux
:, quindi verificare se CPU% / 100 < n threads
. In caso affermativo, si è collegati a IO, ad esempio i blocchi in read
attesa di dati e lo scheduler sta saltando quel processo. Quindi utilizzare ulteriori strumenti come sudo iotop
decidere quale IO è esattamente il problema.
Oppure, se l'esecuzione è rapida e si parametrizza il numero di thread, è possibile vederlo facilmente da time
quel miglioramento delle prestazioni all'aumentare del numero di thread per il lavoro associato alla CPU: cosa significano "reale", "utente" e "sys" in l'uscita del tempo (1)?
Limite RAM-IO: più difficile da dire, poiché il tempo di attesa RAM è incluso nelle CPU%
misurazioni, vedere anche:
Alcune opzioni:
GPU
Le GPU hanno un collo di bottiglia di IO quando trasferisci per la prima volta i dati di input dalla normale RAM leggibile dalla CPU alla GPU.
Pertanto, le GPU possono essere migliori delle CPU solo per le applicazioni associate alla CPU.
Tuttavia, una volta che i dati vengono trasferiti alla GPU, possono operare su quei byte più velocemente della CPU, perché la GPU:
ha una maggiore localizzazione dei dati rispetto alla maggior parte dei sistemi CPU, quindi è possibile accedere ai dati più velocemente per alcuni core rispetto ad altri
sfrutta il parallelismo dei dati e sacrifica la latenza semplicemente ignorando tutti i dati che non sono pronti per essere utilizzati immediatamente.
Poiché la GPU deve operare su grandi dati di input paralleli, è meglio saltare ai dati successivi che potrebbero essere disponibili invece di aspettare che i dati correnti siano disponibili e bloccare tutte le altre operazioni come la CPU fa per lo più
Pertanto la GPU può essere più veloce di una CPU se l'applicazione:
Queste scelte progettuali erano originariamente rivolte all'applicazione del rendering 3D, i cui passaggi principali sono indicati in Cosa sono gli shader in OpenGL e per cosa abbiamo bisogno?
e quindi concludiamo che quelle applicazioni sono legate alla CPU.
Con l'avvento della GPGPU programmabile, possiamo osservare diverse applicazioni GPGPU che servono come esempi di operazioni legate alla CPU:
Elaborazione di immagini con shader GLSL?
Le operazioni di elaborazione delle immagini locali come un filtro di sfocatura sono di natura molto parallela.
È possibile creare una mappa di calore partendo da dati puntuali a 60 volte al secondo?
Stampa di grafici di mappe di calore se la funzione tracciata è abbastanza complessa.
https://www.youtube.com/watch?v=fE0P6H8eK4I "Dinamica dei fluidi in tempo reale: CPU vs GPU" di Jesús Martín Berlanga
Risolvere equazioni differenziali parziali come il equazione di fluidodinamica di Navier Stokes :
Guarda anche:
CPython Global Intepreter Lock (GIL)
Come caso di studio rapido, desidero sottolineare il Python Global Interpreter Lock (GIL): Qual è il blocco dell'interprete globale (GIL) in CPython?
Questo dettaglio dell'implementazione di CPython impedisce a più thread Python di utilizzare in modo efficiente il lavoro associato alla CPU. I documenti di CPython dicono:
Dettagli dell'implementazione di CPython: in CPython, a causa del Global Interpreter Lock, solo un thread può eseguire il codice Python alla volta (anche se alcune librerie orientate alle prestazioni potrebbero superare questa limitazione). Se si desidera che l'applicazione utilizzi meglio le risorse computazionali delle macchine multi-core, si consiglia di utilizzare
multiprocessing
oconcurrent.futures.ProcessPoolExecutor
. Tuttavia, il threading è ancora un modello appropriato se si desidera eseguire più attività associate a I / O contemporaneamente.
Pertanto, qui abbiamo un esempio in cui il contenuto associato alla CPU non è adatto e il limite I / O lo è.
Il limite CPU indica che il programma ha un collo di bottiglia dalla CPU o unità di elaborazione centrale, mentre il limite I / O indica che il programma è un collo di bottiglia tramite I / O o input / output, come leggere o scrivere su disco, rete, ecc.
In generale, quando si ottimizzano i programmi per computer, si cerca di individuare il collo di bottiglia ed eliminarlo. Sapere che il tuo programma è associato alla CPU aiuta a non ottimizzare inutilmente qualcos'altro.
[E per "collo di bottiglia" intendo la cosa che rende il tuo programma più lento di quanto altrimenti avrebbe fatto.]
Un altro modo per esprimere la stessa idea:
Se accelerare la CPU non accelera il programma, potrebbe essere associato a I / O.
Se accelerare l'I / O (ad es. Utilizzando un disco più veloce) non aiuta, il programma potrebbe essere associato alla CPU.
(Ho usato "potrebbe essere" perché è necessario prendere in considerazione altre risorse. La memoria è un esempio.)
Quando il programma è in attesa di I / O (ad es. Lettura / scrittura di un disco o lettura / scrittura di una rete, ecc.), La CPU è libera di svolgere altre attività anche se il programma è fermo. La velocità del tuo programma dipenderà principalmente dalla velocità con cui questo IO può accadere e se vuoi accelerarlo dovrai accelerare l'I / O.
Se il programma esegue molte istruzioni del programma e non è in attesa di I / O, si dice che è associato alla CPU. Accelerare la CPU renderà il programma più veloce.
In entrambi i casi, la chiave per accelerare il programma potrebbe non essere quella di accelerare l'hardware, ma di ottimizzare il programma per ridurre la quantità di IO o CPU di cui ha bisogno, o di fare I / O mentre fa anche un uso intensivo della CPU cose.
Il limite I / O si riferisce a una condizione in cui il tempo necessario per completare un calcolo è determinato principalmente dal periodo trascorso in attesa del completamento delle operazioni di input / output.
Questo è l'opposto di un'attività legata alla CPU. Questa circostanza si presenta quando la velocità con cui i dati vengono richiesti è più lenta della velocità con cui vengono consumati o, in altre parole, si impiega più tempo a richiedere dati che a elaborarli.
Il cuore della programmazione asincrona sono gli oggetti Task e Task, che modellano le operazioni asincrone. Sono supportati da asincrono e attendono parole chiave. Il modello è abbastanza semplice nella maggior parte dei casi:
Per il codice associato a I / O, si attende un'operazione che restituisce un'attività o un'attività all'interno di un metodo asincrono.
Per il codice associato alla CPU, si attende un'operazione avviata su un thread in background con il metodo Task.Run.
La parola chiave wait è dove accade la magia. Fornisce il controllo al chiamante del metodo che ha atteso e alla fine consente a un'interfaccia utente di essere reattiva o un servizio di essere elastico.
Esempio di I / O: download di dati da un servizio Web
private readonly HttpClient _httpClient = new HttpClient();
downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await _httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
Esempio associato alla CPU: esecuzione di un calcolo per un gioco
private DamageResult CalculateDamageDone()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
}
calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
Gli esempi sopra hanno mostrato come è possibile utilizzare asincrono e attendere il lavoro di I / O e CPU. È la chiave che è possibile identificare quando un lavoro che è necessario eseguire è associato a I / O o CPU, poiché può influire notevolmente sulle prestazioni del codice e potenzialmente portare a un uso improprio di determinati costrutti.
Ecco due domande che dovresti porre prima di scrivere qualsiasi codice:
Il tuo codice "aspetterà" qualcosa, come i dati di un database?
- Se la tua risposta è "sì", il tuo lavoro è associato a I / O.
Il tuo codice eseguirà un calcolo molto costoso?
- Se hai risposto "sì", il tuo lavoro è associato alla CPU.
Se il lavoro che hai è associato a I / O, usa asincronizzazione e attendi senza Task.Run . Non utilizzare la Libreria parallela attività. La ragione di ciò è descritta nell'articolo di Async in Depth .
Se il lavoro che hai è legato alla CPU e ti preoccupi della reattività, usa async e attendi, ma spawn il lavoro su un altro thread con Task.Run. Se il lavoro è appropriato per la concorrenza e il parallelismo, è necessario considerare anche l'uso della Libreria parallela attività .
Un'applicazione è associata alla CPU quando le prestazioni aritmetiche / logiche / a virgola mobile (A / L / FP) durante l'esecuzione sono per lo più vicine alle prestazioni di picco teoriche del processore (dati forniti dal produttore e determinati dalle caratteristiche del processore: numero di core, frequenza, registri, ALU, FPU, ecc.).
Le prestazioni della sbirciatina sono molto difficili da ottenere nelle applicazioni del mondo reale, per non dire impossibili. La maggior parte delle applicazioni accede alla memoria in diverse parti dell'esecuzione e il processore non esegue operazioni A / L / FP durante diversi cicli. Questo è chiamato Von Neumann Limitation a causa della distanza esistente tra la memoria e il processore.
Se si desidera essere vicini alle massime prestazioni della CPU, una strategia potrebbe essere quella di provare a riutilizzare la maggior parte dei dati nella memoria cache per evitare di richiedere dati dalla memoria principale. Un algoritmo che sfrutta questa funzionalità è la moltiplicazione matrice-matrice (se entrambe le matrici possono essere archiviate nella memoria cache). Ciò accade perché se le matrici sono di dimensioni, n x n
è necessario eseguire le 2 n^3
operazioni utilizzando solo 2 n^2
numeri di dati FP. D'altra parte, l'aggiunta di matrici, ad esempio, è un'applicazione con meno limiti di CPU o memoria rispetto alla moltiplicazione di matrici poiché richiede solo n^2
FLOP con gli stessi dati.
Nella figura seguente sono mostrati i FLOP ottenuti con un algoritmo ingenuo per l'aggiunta della matrice e la moltiplicazione della matrice in un Intel i5-9300H:
Si noti che, come previsto, le prestazioni della moltiplicazione della matrice sono maggiori dell'aggiunta alla matrice. Questi risultati possono essere riprodotti eseguendo test/gemm
e test/matadd
disponibili in questo repository .
Suggerisco anche di vedere il video di J. Dongarra su questo effetto.
Processo associato I / O: - Se la maggior parte della durata di un processo viene spesa nello stato I / O, il processo è ai / o processo associato.esempio: -calculator, internet explorer
Processo associato alla CPU: - Se la maggior parte della durata del processo viene trascorsa in CPU, si tratta di un processo associato alla CPU.