Riassunto: La ricerca e lo sfruttamento del parallelismo (a livello di istruzione) in un programma a thread singolo viene eseguito esclusivamente nell'hardware, dal core della CPU su cui è in esecuzione. E solo su una finestra di circa duecento istruzioni, non un riordino su larga scala.
I programmi a thread singolo non traggono alcun vantaggio dalle CPU multi-core, tranne per il fatto che altre cose possono essere eseguite sugli altri core invece di perdere tempo dall'attività a thread singolo.
il sistema operativo organizza le istruzioni di tutti i thread in modo tale che non si aspettino l'un l'altro.
Il sistema operativo NON guarda all'interno dei flussi di istruzioni dei thread. Pianifica solo i thread in core.
In realtà, ogni core esegue la funzione di pianificazione del sistema operativo quando deve capire cosa fare dopo. La pianificazione è un algoritmo distribuito. Per comprendere meglio le macchine multi-core, pensa a ciascun core come a eseguire il kernel separatamente. Proprio come un programma multi-thread, il kernel è scritto in modo che il suo codice su un core possa interagire in modo sicuro con il suo codice su altri core per aggiornare le strutture di dati condivisi (come l'elenco dei thread che sono pronti per essere eseguiti.
Ad ogni modo, il sistema operativo è coinvolto nell'aiutare i processi multi-thread a sfruttare il parallelismo a livello di thread che deve essere esplicitamente esposto scrivendo manualmente un programma multi-thread . (O da un compilatore a parallelizzazione automatica con OpenMP o qualcosa del genere).
Quindi il front-end della CPU organizza ulteriormente tali istruzioni distribuendo un thread su ciascun core e distribuisce istruzioni indipendenti da ciascun thread tra tutti i cicli aperti.
Un core della CPU esegue solo un flusso di istruzioni, se non viene arrestato (inattivo fino all'interruzione successiva, ad esempio l'interruzione del timer). Spesso si tratta di un thread, ma potrebbe anche essere un gestore di interrupt del kernel o un codice del kernel diverso se il kernel ha deciso di fare qualcosa di diverso dal semplice ritorno al thread precedente dopo la gestione e l'interrupt o la chiamata di sistema.
Con HyperThreading o altri progetti SMT, un core fisico della CPU si comporta come più core "logici". L'unica differenza dal punto di vista del sistema operativo tra una CPU quad-core-con-hyperthreading (4c8t) e una semplice macchina a 8 core (8c8t) è che un sistema operativo compatibile con HT tenterà di pianificare i thread per separare i core fisici in modo che non competere tra loro. Un sistema operativo che non era a conoscenza dell'hyperthreading vedrebbe solo 8 core (a meno che non disabiliti HT nel BIOS, ne rileverebbe solo 4).
Il termine " front-end" si riferisce alla parte di un core della CPU che recupera il codice macchina, decodifica le istruzioni e le immette nella parte fuori core del core . Ogni core ha il suo front-end ed è parte del core nel suo insieme. Le istruzioni che recupera sono ciò che la CPU è attualmente in esecuzione.
All'interno della parte fuori servizio del core, le istruzioni (o uops) vengono inviate alle porte di esecuzione quando i loro operandi di input sono pronti e c'è una porta di esecuzione libera. Questo non deve accadere nell'ordine del programma, quindi è così che una CPU OOO può sfruttare il parallelismo a livello di istruzione all'interno di un singolo thread .
Se sostituisci "core" con "unità di esecuzione" nella tua idea, sei vicino alla correzione. Sì, la CPU distribuisce istruzioni / uops indipendenti alle unità di esecuzione in parallelo. (Ma c'è una confusione terminologica, dal momento che hai detto "front-end" quando in realtà è il programmatore di istruzioni della CPU aka Reservation Station che prende le istruzioni pronte per l'esecuzione).
L'esecuzione fuori ordine può trovare ILP solo a livello locale, solo fino a duecento istruzioni, non tra due cicli indipendenti (a meno che non siano brevi).
Ad esempio, l'equivalente di questo
int i=0,j=0;
do {
i++;
j++;
} while(42);
funzionerà alla stessa velocità dello stesso loop incrementando solo un contatore su Intel Haswell. i++
dipende solo dal valore precedente di i
, mentre j++
dipende solo dal valore precedente di j
, quindi le due catene di dipendenza possono funzionare in parallelo senza rompere l'illusione di tutto ciò che viene eseguito nell'ordine del programma.
Su x86, il loop sarebbe simile a questo:
top_of_loop:
inc eax
inc edx
jmp .loop
Haswell ha 4 porte di esecuzione intere e tutte hanno unità sommatrici, quindi può sostenere un throughput di fino a 4 inc
istruzioni per clock se sono tutte indipendenti. (Con latenza = 1, quindi sono necessari solo 4 registri per massimizzare il throughput mantenendo 4 inc
istruzioni in volo. Contrastare questo con vettore-FP MUL o FMA: latenza = 5 throughput = 0,5 sono necessari 10 accumulatori vettoriali per mantenere 10 FMA in volo per massimizzare il throughput. E ogni vettore può essere 256b, con 8 float a precisione singola).
Il ramo preso è anche un collo di bottiglia: un ciclo richiede sempre almeno un intero orologio per iterazione, perché il throughput del ramo preso è limitato a 1 per orologio. Potrei inserire un'altra istruzione all'interno del loop senza ridurre le prestazioni, a meno che non legga / scriva eax
o edx
nel qual caso allungherebbe quella catena di dipendenze. Mettere altre 2 istruzioni nel loop (o un'istruzione multi-uop complessa) creerebbe un collo di bottiglia sul front-end, dal momento che può emettere solo 4 uops per clock nel core fuori servizio. (Vedi queste domande e risposte SO per alcuni dettagli su cosa succede per i loop che non sono un multiplo di 4 uops: il loop-buffer e la cache uop rendono le cose interessanti.)
In casi più complessi, trovare il parallelismo richiede una finestra più ampia di istruzioni . (es. forse c'è una sequenza di 10 istruzioni che dipendono tutte l'una dall'altra, quindi alcune indipendenti).
La capacità del buffer di riordino è uno dei fattori che limita le dimensioni della finestra fuori ordine. Su Intel Haswell, sono 192 uops. (E puoi anche misurarlo sperimentalmente , insieme alla capacità di rinominare il registro (dimensione del file di registro).) I core della CPU a basso consumo come ARM hanno dimensioni ROB molto più piccole, se eseguono un'esecuzione fuori servizio.
Si noti inoltre che le CPU devono essere pipeline, oltre che fuori servizio. Quindi deve recuperare e decodificare le istruzioni molto prima di quelle eseguite, preferibilmente con un throughput sufficiente per riempire i buffer dopo aver perso i cicli di recupero. I rami sono difficili, perché non sappiamo nemmeno da dove recuperare se non sappiamo da che parte è andato un ramo. Questo è il motivo per cui la previsione del ramo è così importante. (E perché le CPU moderne usano l'esecuzione speculativa: indovinano da che parte andrà un ramo e iniziano a recuperare / decodificare / eseguire quel flusso di istruzioni. Quando viene rilevato un errore, tornano all'ultimo stato noto ed eseguono da lì.)
Se vuoi saperne di più sugli interni della CPU, ci sono alcuni collegamenti nel wiki del tag x86 di Stackoverflow , inclusi la guida al microarca di Agner Fog e i dettagli di David Kanter con diagrammi di CPU Intel e AMD. Dal suo articolo di microarchitettura Intel Haswell , questo è il diagramma finale dell'intera pipeline di un core Haswell (non dell'intero chip).
Questo è uno schema a blocchi di un singolo core della CPU . Una CPU quad-core ne ha 4 su un chip, ognuna con le proprie cache L1 / L2 (condividendo una cache L3, controller di memoria e connessioni PCIe ai dispositivi di sistema).
So che questo è estremamente complicato. L'articolo di Kanter mostra anche parti di questo per parlare del frontend separatamente dalle unità di esecuzione o dalle cache, per esempio.