Come funziona un singolo thread su più core?


61

Sto cercando di capire, ad alto livello, come singoli thread corrono su più core. Di seguito è la mia migliore comprensione. Non credo sia corretto però.

Sulla base della mia lettura di Hyper-threading , sembra che il sistema operativo organizzi le istruzioni di tutti i thread in modo tale che non si stiano aspettando l'un l'altro. 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.

Quindi, se esiste un solo thread, il sistema operativo non eseguirà alcuna ottimizzazione. Tuttavia, il front-end della CPU distribuirà set di istruzioni indipendenti tra ciascun core.

Secondo https://stackoverflow.com/a/15936270 , un linguaggio di programmazione specifico può creare più o meno thread, ma è irrilevante nel determinare cosa fare con tali thread. Il sistema operativo e la CPU gestiscono ciò, quindi ciò accade indipendentemente dal linguaggio di programmazione utilizzato.

inserisci qui la descrizione dell'immagine

Giusto per chiarire, sto chiedendo di eseguire un singolo thread su più core, non di eseguire più thread su un singolo core.

Cosa c'è di sbagliato nel mio sommario? Dove e come vengono suddivise le istruzioni di un thread tra più core? Il linguaggio di programmazione è importante? So che questo è un argomento ampio; Spero in una comprensione di alto livello.


6
Una serie di istruzioni per un singolo thread software può essere eseguita su molti core, ma non contemporaneamente.
Kroltan,

1
Stai mescolando thread software (che coinvolgono lo scheduler del sistema operativo) e thread hardware o HyperThreading (una funzionalità della CPU che fa comportare un core come due).
ugoren,

2
Ho 20 conducenti e 4 camion. Come è possibile che un conducente possa consegnare pacchi con due camion? Come è possibile che un camion possa avere più conducenti? La risposta ad entrambe le domande è la stessa. Alternarsi.
Eric Lippert,

Risposte:


84

Il sistema operativo offre fasce orarie di CPU ai thread che possono essere eseguiti.

Se esiste un solo core, il sistema operativo pianifica l'esecuzione del thread più idoneo su quel core per un intervallo di tempo. Dopo il completamento di un intervallo di tempo, o quando il thread in esecuzione si blocca su IO o quando il processore viene interrotto da eventi esterni, il sistema operativo rivaluta il thread da eseguire successivamente (e potrebbe scegliere di nuovo lo stesso thread o uno diverso).

L'eleggibilità per l'esecuzione consiste in variazioni di correttezza, priorità e prontezza e con questo metodo vari thread ottengono fasce orarie, alcune più di altre.

Se sono presenti più core, N, il sistema operativo pianifica l'esecuzione dei thread N più idonei sui core.

L'affinità del processore è un fattore di efficienza. Ogni volta che una CPU esegue un thread diverso rispetto a prima, tende a rallentare un po 'perché la sua cache è calda per il thread precedente, ma fredda per quella nuova. Pertanto, l'esecuzione dello stesso thread sullo stesso processore su numerosi intervalli di tempo rappresenta un vantaggio in termini di efficienza.

Tuttavia, il sistema operativo è libero di offrire intervalli di tempo di un thread su CPU diverse e può ruotare attraverso tutte le CPU su intervalli di tempo diversi. Tuttavia, come dice @ gnasher729 , non può eseguire contemporaneamente un thread su più CPU.

L'hyperthreading è un metodo hardware che consente a un singolo core CPU avanzato di supportare l'esecuzione simultanea di due o più thread diversi . (Tale CPU può offrire thread aggiuntivi a costi inferiori nel settore immobiliare in silicio rispetto ai core completi aggiuntivi.) Questo core CPU avanzato deve supportare uno stato aggiuntivo per gli altri thread, come i valori del registro CPU, e ha anche uno stato e un comportamento di coordinamento che consente la condivisione di unità funzionali all'interno di quella CPU senza confondere i thread.

L'hyperthreading, sebbene tecnicamente impegnativo dal punto di vista hardware, dal punto di vista del programmatore, il modello di esecuzione è semplicemente quello di core CPU aggiuntivi piuttosto che qualcosa di più complesso. Quindi, il sistema operativo vede ulteriori core della CPU, sebbene ci siano alcuni nuovi problemi di affinità del processore poiché diversi thread hyperthreaded condividono l'architettura cache di un core CPU.


Potremmo ingenuamente pensare che due thread in esecuzione su un core hyperthreadded eseguano ciascuno la metà della velocità di ognuno con il proprio core completo. Ma questo non è necessariamente il caso, poiché l'esecuzione di un singolo thread è piena di cicli lenti e una parte di essi può essere utilizzata dall'altro thread hyperthreaded. Inoltre, anche durante cicli non lenti, un thread può utilizzare unità funzionali diverse rispetto all'altra, in modo che possa verificarsi un'esecuzione simultanea. La CPU potenziata per l'hyperthreading potrebbe avere alcune unità funzionali molto usate in particolare per supportare questo.


3
"Pertanto, eseguire lo stesso thread sullo stesso processore su numerosi intervalli di tempo è un vantaggio in termini di efficienza." Non dovrebbero essere fasce orarie contigue ? Altrimenti, le cache verrebbero cancellate da altri thread, no? +1 per una bella spiegazione.
jpmc26,

2
@Luaan: HT è spesso buono, ma la situazione non è così semplice come la descrivi. La larghezza di banda del problema del front-end (4 uops per clock su Intel, 6 su Ryzen) è equamente condivisa tra i thread (a meno che uno non sia bloccato). Se questo è il collo di bottiglia, allora come ho detto HT non aiuta affatto. Non è raro che Skylake si avvicini a questo in un loop ben sintonizzato, se c'è un mix di carichi, ALU e negozi ... I transistor sono economici (e non possono essere tutti commutati contemporaneamente o la CPU si scioglierebbe), così le moderne CPU x86 hanno più porte di esecuzione di quelle che il front-end può alimentare (con molte unità di esecuzione replicate ...
Peter Cordes,

2
... su più porte) ... Questo potrebbe sembrare uno spreco, ma spesso un loop utilizzerà solo un tipo di unità di esecuzione ALU alla volta, quindi avere duplicati di tutto significa che qualunque tipo di codice è in esecuzione, ci sono più porte per le sue istruzioni. Quindi il motivo che hai citato per beneficiare di HT non è così comune, poiché la maggior parte del codice ha alcuni carichi e / o negozi che occupano larghezza di banda front-end e ciò che resta spesso non è sufficiente per saturare le unità di esecuzione.
Peter Cordes,

2
@Luaan: Inoltre, nelle CPU Intel le unità di esecuzione integer e FP / vector condividono le stesse porte di esecuzione . Ad esempio, le unità FP FMA / mul / add si trovano sulle porte 0/1. Ma il moltiplicatore di numeri interi è anche sulla porta 1 e le operazioni di numero intero semplici possono essere eseguite su una qualsiasi delle 4 porte di esecuzione (diagramma nella mia risposta). Un secondo thread che utilizza la larghezza di banda problematica in eccesso li rallenterà entrambi anche se non competono per le unità di esecuzione, ma spesso c'è un guadagno netto di throughput se non competono troppo male per la cache. Anche il codice high-throughput ottimizzato come x264 / x265 (codificatori video) beneficia di circa il 15% su Skylake di HT.
Peter Cordes,

3
@luaan Oltre a ciò che ha detto Peter, la tua affermazione che "Questo era il ragionamento originale dietro HT" non è corretta. Il ragionamento originale dietro HT era che la microarchitettura NetBurst aveva allungato la pipeline in misura così estrema (allo scopo di aumentare la velocità di clock) che le previsioni errate del ramo e altre bolle della pipeline hanno assolutamente ucciso le prestazioni. HT era una delle soluzioni di Intel per ridurre al minimo il tempo in cui le unità di esecuzione di questo grande chip costoso restavano inattive a causa delle bolle nella pipeline: il codice di altri thread poteva essere inserito ed eseguito in quei buchi.
Cody Grey,

24

Non esiste un singolo thread in esecuzione su più core contemporaneamente.

Ciò non significa, tuttavia, che le istruzioni di un thread non possano essere eseguite in parallelo. Esistono meccanismi chiamati pipeline di istruzioni ed esecuzione fuori ordine che lo consentono. Ogni core ha molte risorse ridondanti che non sono utilizzate da semplici istruzioni, quindi più istruzioni di questo tipo possono essere eseguite insieme (purché la successiva non dipenda dal risultato precedente). Tuttavia, ciò accade ancora all'interno di un singolo core.

L'hyper-threading è una specie di variante estrema di questa idea, in cui un core non solo esegue le istruzioni da un thread in parallelo, ma mescola le istruzioni di due thread diversi per ottimizzare ulteriormente l'utilizzo delle risorse.

Voci Wikipedia correlate: Pipeline di istruzioni , esecuzione fuori ordine .


3
Non possono funzionare contemporaneamente, ma possono funzionare in parallelo? Queste non sono la stessa cosa?
Evorlor,

10
@Evorlor La cosa chiave qui è la differenza tra un nucleo e un'unità di esecuzione. Un singolo thread può essere eseguito solo su un core, ma un processore può utilizzare l'analisi dinamica per capire quali istruzioni eseguite da un core non dipendono l'una dall'altra ed eseguirle simultaneamente su unità di esecuzione diverse. Un core può avere diverse unità di esecuzione.
user1937198

3
@Evorlor: una CPU fuori servizio può trovare e sfruttare il parallelismo a livello di istruzione all'interno del flusso di istruzioni di un singolo thread. ad esempio, spesso le istruzioni che aggiornano un contatore di cicli sono indipendenti da alcune delle altre operazioni svolte da un ciclo. O in un a[i] = b[i] + c[i]ciclo, ogni iterazione è indipendente, quindi carichi, aggiunte e negozi di diverse iterazioni possono essere in volo contemporaneamente. Deve preservare l'illusione che le istruzioni vengano eseguite nell'ordine del programma, ma ad esempio un archivio che manca nella cache non ritarda il thread (fino a quando non esaurisce lo spazio nel buffer dell'archivio).
Peter Cordes,

3
@ user1937198: la frase "analisi dinamica" si adatterebbe meglio a un compilatore JIT. Le CPU fuori servizio non analizzano realmente ; è più come un avido algoritmo che esegue qualunque istruzione sia stata decodificata ed emessa e abbia i loro input pronti. (La finestra di riordino fuori ordine è limitata da alcune risorse microarchitetturali, ad esempio Intel Sandybridge ha una dimensione del buffer ReOrder di 168 uops. Vedere anche la misurazione sperimentale della dimensione ROB ). Tutto implementato con macchine a stati hardware per gestire 4 uops per clock.
Peter Cordes,

3
@Luaan sì, è stata un'idea interessante, ma i compilatori AOT non sono ancora abbastanza intelligenti da sfruttarlo appieno. Inoltre, Linus Torvalds (e altri) hanno sostenuto che esporre che gran parte degli interni della pipeline è un grande vincolo per i progetti futuri. ad esempio, non è possibile aumentare realmente la larghezza della tubazione senza modificare l'ISA. Oppure costruisci una CPU che tiene traccia delle dipendenze nel solito modo e magari genera due gruppi VLIW in parallelo, ma poi hai perso il vantaggio della complessità della CPU di EPIC ma hai ancora i lati negativi (perdi la larghezza di banda del problema quando il compilatore non può riempire una parola).
Peter Cordes,

22

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 incistruzioni per clock se sono tutte indipendenti. (Con latenza = 1, quindi sono necessari solo 4 registri per massimizzare il throughput mantenendo 4 incistruzioni 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 eaxo edxnel 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).

Gasdotto completo Haswell

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.


2
"La ricerca e lo sfruttamento del parallelismo (a livello di istruzione) in un programma a thread singolo viene eseguito esclusivamente nell'hardware" Si noti che ciò si applica solo agli ISA convenzionali, non ai VLIW in cui l'ILP viene determinato completamente dal compilatore o programmatore o in cooperazione tra hardware e software.
Hadi Brais,

1
@ user7813604: sì. L'hyperthreading non può parallelizzare un singolo thread. Fa il contrario: esegue più thread su un core, riducendo le prestazioni per thread ma aumentando la velocità complessiva.
Peter Cordes,

1
@ user7813604: Il punto centrale di ILP è scoprire quali istruzioni possono essere eseguite in parallelo, pur mantenendo l' illusione che ciascuna istruzione sia stata eseguita in ordine, ognuna che termina prima che inizi quella successiva. Una CPU con pipeline scalare potrebbe dover arrestarsi a volte per dipendenze se la latenza è superiore a 1. Ma è un affare ancora più grande per le CPU superscalari.
Peter Cordes,

1
@utente7813604: sì, la mia risposta lo usa letteralmente come esempio. Haswell, ad esempio, può eseguire fino a 4 incistruzioni nello stesso ciclo di clock, alle sue 4 unità di esecuzione ALU intere.
Peter Cordes,

1
@ user7813604: Sì, ILP è quanto può essere eseguito in parallelo. Una CPU reale avrà una capacità limitata di trovare e sfruttare ILP eseguendola in parallelo all'interno di un singolo core, ad esempio fino a 4 superscalar in Intel. Questa risposta cerca di spiegarlo con esempi.
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.