web-dev-qa-db-fra.com

Comment fonctionne un seul thread sur plusieurs cœurs?

J'essaie de comprendre, à un niveau élevé, comment les threads uniques s'exécutent sur plusieurs cœurs. Voici ma meilleure compréhension. Je ne pense pas que ce soit correct cependant.

Sur la base de ma lecture de Hyper-threading , il semble que le système d'exploitation organise les instructions de tous les threads de telle manière qu'ils ne s'attendent pas les uns les autres. Ensuite, la partie frontale du CPU organise ces instructions en distribuant un thread à chaque cœur et distribue des instructions indépendantes de chaque thread entre tous les cycles ouverts.

Donc, s'il n'y a qu'un seul thread, le système d'exploitation ne fera aucune optimisation. Cependant, le frontal du CPU distribuera des jeux d'instructions indépendants entre chaque cœur.

Selon https://stackoverflow.com/a/1593627 , un langage de programmation spécifique peut créer plus ou moins de threads, mais il n'est pas pertinent pour déterminer quoi faire avec ces threads. Le système d'exploitation et le processeur gèrent cela, donc cela se produit quel que soit le langage de programmation utilisé.

enter image description here

Juste pour clarifier, je pose des questions sur un seul thread exécuté sur plusieurs cœurs, pas sur l'exécution de plusieurs threads sur un seul noyau.

Quel est le problème avec mon résumé? Où et comment les instructions d'un thread sont-elles réparties entre plusieurs cœurs? Le langage de programmation est-il important? Je sais que c'est un vaste sujet; J'espère en avoir une compréhension de haut niveau.

65
Evorlor

Le système d'exploitation offre tranche de temps s de CPU aux threads éligibles pour s'exécuter.

S'il n'y a qu'un seul cœur, le système d'exploitation planifie le thread le plus éligible pour s'exécuter sur ce cœur pour une tranche de temps. Une fois la tranche horaire terminée, ou lorsque le thread en cours d'exécution se bloque sur les E/S, ou lorsque le processeur est interrompu par des événements externes, le système d'exploitation réévalue le thread à exécuter ensuite (et il peut choisir à nouveau le même thread ou un autre).

L'éligibilité à courir consiste en des variations d'équité et de priorité et de préparation, et par cette méthode, divers threads obtiennent des tranches de temps, certaines plus que d'autres.

S'il existe plusieurs cœurs, N, le système d'exploitation planifie les N threads les plus éligibles pour s'exécuter sur les cœurs.

Affinité du processeur est une considération d'efficacité. Chaque fois qu'un CPU exécute un thread différent qu'auparavant, il a tendance à ralentir un peu car son cache est chaud pour le thread précédent, mais froid pour le nouveau. Ainsi, l'exécution du même thread sur le même processeur sur de nombreuses tranches de temps est un avantage d'efficacité.

Cependant, le système d'exploitation est libre de proposer des tranches de temps à un thread sur différents CPU, et il peut tourner sur tous les CPU sur des tranches de temps différentes. Il ne peut pas, cependant, comme @ gnasher729 dit , exécuter un thread sur plusieurs CPU simultanément.

L'hyperthreading est une méthode dans le matériel par laquelle un seul amélioré noyau CPU peut prendre en charge l'exécution de deux ou plus different threads simultanément. (Un tel processeur peut offrir des threads supplémentaires à moindre coût dans l'immobilier en silicium que des cœurs complets supplémentaires.) Ce cœur de processeur amélioré doit prendre en charge un état supplémentaire pour les autres threads, tels que les valeurs de registre du processeur, et possède également un état et un comportement de coordination qui permet le partage d'unités fonctionnelles au sein de ce CPU sans confondre les threads.

Hyperthreading, bien que techniquement difficile du point de vue matériel, du point de vue du programmeur, le modèle d'exécution est simplement celui de cœurs de processeur supplémentaires plutôt que quelque chose de plus complexe. Ainsi, le système d'exploitation voit des cœurs de processeur supplémentaires, bien qu'il y ait de nouveaux problèmes d'affinité de processeur, car plusieurs threads hyperthreadés partagent l'architecture de cache d'un cœur de processeur.


Nous pourrions naïvement penser que deux threads s'exécutant sur un noyau hyperthreadded s'exécutent chacun à moitié aussi vite qu'ils le feraient chacun avec leur propre noyau complet. Mais ce n'est pas nécessairement le cas, car l'exécution d'un seul thread est pleine de cycles lâches, et une partie d'entre eux peut être utilisée par l'autre thread hyperthreadé. De plus, même pendant les cycles sans relâchement, un thread peut utiliser des unités fonctionnelles différentes de l'autre afin qu'une exécution simultanée puisse se produire. Le processeur amélioré pour l'hyperthreading peut avoir un peu plus de certaines unités fonctionnelles très utilisées spécialement pour prendre en charge cela.

84
Erik Eidt

Il n'y a rien de tel qu'un seul thread s'exécutant sur plusieurs cœurs simultanément.

Cela ne signifie pas, cependant, que les instructions d'un thread ne peuvent pas être exécutées en parallèle. Il existe des mécanismes appelés pipelining des instructions et exécution dans le désordre qui permettez-ceci. Chaque cœur possède de nombreuses ressources redondantes qui ne sont pas utilisées par de simples instructions, de sorte que plusieurs de ces instructions peuvent être exécutées ensemble (tant que la suivante ne dépend pas du résultat précédent). Cependant, cela se produit toujours dans un seul cœur.

L'hyper-threading est une sorte de variante extrême de cette idée, dans laquelle un noyau exécute non seulement des instructions d'un thread en parallèle, mais mélange des instructions de deux threads différents pour optimiser encore plus l'utilisation des ressources.

Entrées Wikipedia associées: Pipelining des instructions , exécution dans le désordre .

24
Frax

résumé: La recherche et l'exploitation du parallélisme (au niveau de l'instruction) dans un programme à un seul thread se fait uniquement sur le matériel, par le noyau du processeur sur lequel il fonctionne. Et seulement sur une fenêtre de quelques centaines d'instructions, pas de réorganisation à grande échelle.

Les programmes à thread unique ne tirent aucun avantage des processeurs multicœurs, sauf que autre les choses peuvent s'exécuter sur les autres cœurs au lieu de prendre du temps sur la tâche à thread unique.


l'OS organise les instructions de tous les threads de manière à ce qu'ils ne s'attendent pas les uns les autres.

L'OS ne regarde PAS à l'intérieur des flux d'instructions des threads. Il planifie uniquement les threads vers les cœurs.

En fait, chaque cœur exécute la fonction de planificateur du système d'exploitation lorsqu'il a besoin de savoir quoi faire ensuite. La planification est un algorithme distribué. Pour mieux comprendre les machines multicœurs, pensez à chaque cœur comme exécutant le noyau séparément. Tout comme un programme multithread, le noyau est écrit de sorte que son code sur un noyau puisse interagir en toute sécurité avec son code sur d'autres cœurs pour mettre à jour les structures de données partagées (comme la liste des threads qui sont prêts à fonctionner).

Quoi qu'il en soit, le système d'exploitation contribue à aider les processus multithreads à exploiter le parallélisme au niveau des threads qui doit être explicitement exposé en écrivant manuellement un programme multithread . (Ou par un compilateur à parallélisation automatique avec OpenMP ou quelque chose).

Ensuite, la partie frontale du CPU organise ces instructions en distribuant un thread à chaque cœur et distribue des instructions indépendantes de chaque thread entre tous les cycles ouverts.

Un cœur de processeur exécute un seul flux d'instructions s'il n'est pas arrêté (endormi jusqu'à la prochaine interruption, par exemple, interruption du minuteur). Il s'agit souvent d'un thread, mais il peut également s'agir d'un gestionnaire d'interruption du noyau ou d'un code de noyau divers si le noyau décide de faire autre chose que de simplement revenir au thread précédent après la gestion et l'interruption ou l'appel système.

Avec HyperThreading ou d'autres conceptions SMT, un cœur de processeur physique agit comme plusieurs cœurs "logiques". La seule différence du point de vue du système d'exploitation entre un processeur quadricœur avec hyperthreading (4c8t) et une machine ordinaire à 8 cœurs (8c8t) est qu'un système d'exploitation compatible HT tentera de planifier des threads pour séparer les cœurs physiques afin qu'ils ne le fassent pas. t rivaliser. Un système d'exploitation qui ne connaissait pas l'hyperthreading ne verrait que 8 cœurs (sauf si vous désactivez HT dans le BIOS, il n'en détecterait que 4).


Le terme " frontal" fait référence à la partie d'un cœur de processeur qui récupère le code machine, décode les instructions et les envoie dans la partie hors service du cœur . Chaque cœur a son propre front-end, et il fait partie du cœur dans son ensemble. Les instructions qu'il récupère sont ce que le CPU exécute actuellement.

À l'intérieur de la partie hors service du noyau, des instructions (ou uops) sont envoyées aux ports d'exécution lorsque leurs opérandes d'entrée sont prêts et qu'il y a un port d'exécution libre. Cela ne doit pas se produire dans l'ordre du programme, donc c'est ainsi qu'un processeur OOO peut exploiter le parallélisme au niveau de l'instruction au sein d'un seul thread .

Si vous remplacez "core" par "execution unit" dans votre idée, vous êtes sur le point de corriger. Oui, le CPU distribue des instructions/uops indépendantes aux unités d'exécution en parallèle. (Mais il y a une confusion terminologique, puisque vous avez dit "front-end" alors que c'est vraiment le planificateur d'instructions du CPU, alias Reservation Station, qui sélectionne les instructions prêtes à être exécutées).

L'exécution dans le désordre ne peut trouver ILP qu'à un niveau très local, seulement jusqu'à quelques centaines d'instructions, pas entre deux boucles indépendantes (sauf si elles sont courtes).


Par exemple, l'équivalent asm de cette

int i=0,j=0;
do {
    i++;
    j++;
} while(42);

s'exécutera à peu près aussi rapidement que la même boucle en incrémentant uniquement un compteur sur Intel Haswell. i++ dépend uniquement de la valeur précédente de i, tandis que j++ ne dépend que de la valeur précédente de j, donc les deux chaînes de dépendance peuvent fonctionner en parallèle sans briser l'illusion de tout ce qui est exécuté dans l'ordre du programme.

Sur x86, la boucle ressemblerait à ceci:

top_of_loop:
    inc eax
    inc edx
    jmp .loop

Haswell dispose de 4 ports d'exécution entiers, et tous ont des unités d'addition, de sorte qu'il peut supporter jusqu'à 4 inc instructions par horloge si elles sont toutes indépendantes. (Avec latence = 1, vous n'avez donc besoin que de 4 registres pour maximiser le débit en gardant 4 inc instructions en vol. Comparez cela avec vector-FP MUL ou FMA: latence = 5 débit = 0,5 a besoin de 10 accumulateurs vectoriels pour garder 10 FMA en vol pour maximiser le débit. Et chaque vecteur peut être 256b, avec 8 flotteurs simple précision).

La dérivation est également un goulot d'étranglement: une boucle prend toujours au moins une horloge entière par itération, car le débit de dérivation est limité à 1 par horloge. Je pourrais mettre une instruction de plus à l'intérieur de la boucle sans réduire les performances, sauf si elle lit/écrit également eax ou edx auquel cas cela allongerait cette chaîne de dépendance. Mettre 2 instructions supplémentaires dans la boucle (ou une instruction multi-uop complexe) créerait un goulot d'étranglement sur le front-end, car il ne peut émettre que 4 uops par horloge dans le noyau hors service. (Voir this SO Q&A pour quelques détails sur ce qui se passe pour les boucles qui ne sont pas un multiple de 4 uops: le tampon de boucle et le cache uop rendent les choses intéressantes). )


Dans les cas plus complexes, trouver le parallélisme nécessite de regarder une plus grande fenêtre d'instructions . (par exemple, il y a peut-être une séquence de 10 instructions qui dépendent toutes les unes des autres, puis certaines indépendantes).

La capacité du tampon de réorganisation est l'un des facteurs qui limite la taille de la fenêtre hors service. Sur Intel Haswell, c'est 192 uops. (Et vous pouvez même mesurer expérimentalement , ainsi que la capacité de renommage de registre (taille de fichier de registre).) Les cœurs de processeur à faible puissance comme ARM ont un ROB beaucoup plus petit tailles, si elles exécutent du tout dans le désordre.

Notez également que les processeurs doivent être pipelinés, ainsi que hors service. Il doit donc récupérer et décoder les instructions bien avant celles qui sont exécutées, de préférence avec un débit suffisant pour remplir les tampons après avoir raté tout cycle de récupération. Les branches sont délicates, car nous ne savons même pas où aller chercher si nous ne savons pas dans quelle direction une branche est allée. C'est pourquoi la prédiction de branche est si importante. (Et pourquoi les processeurs modernes utilisent l'exécution spéculative: ils devinent dans quelle direction une branche ira et commenceront à récupérer/décoder/exécuter ce flux d'instructions. Lorsqu'une erreur de prévision est détectée, ils reviennent au dernier état connu et s'exécutent à partir de là.)

Si vous souhaitez en savoir plus sur les composants internes du processeur, il existe des liens dans le Stackoverflow wiki de balise x86 , y compris vers le guide des microarches d'Agner Fog , et vers les descriptions détaillées de David Kanter avec des diagrammes des processeurs Intel et AMD. D'après son rédaction de la microarchitecture Intel Haswell , voici le schéma final de l'ensemble du pipeline d'un noyau Haswell (pas de la puce entière).

Ceci est un schéma de principe d'un noyau de processeur unique. Un processeur quadricœur en a 4 sur une puce, chacun avec ses propres caches L1/L2 (partageant un cache L3, des contrôleurs de mémoire et des connexions PCIe aux périphériques système).

Haswell full pipeline

Je sais que c'est extrêmement compliqué. L'article de Kanter en montre également des parties pour parler du frontend séparément des unités d'exécution ou des caches, par exemple.

23
Peter Cordes