Récapitulatif: La recherche et l'exploitation du parallélisme ( au niveau instruction) dans un programme mono-thread se fait uniquement de manière matérielle, par le cœur du processeur sur lequel il s'exécute. Et seulement par-dessus une fenêtre de quelques centaines d'instructions, pas de réorganisation à grande échelle.
Les programmes à un seul thread ne tirent aucun avantage des processeurs multicœurs, à la différence que d' autres tâches peuvent s'exécuter sur les autres cœurs au lieu de prendre du temps pour la tâche à un seul thread.
le système d'exploitation organise les instructions de tous les threads de manière à ce qu'ils ne s'attendent pas les uns les autres.
Le système d'exploitation ne regarde pas à l'intérieur des flux d'instructions des threads. Il ne planifie que les threads vers les cœurs.
En réalité, chaque cœur exécute la fonction de planificateur du système d'exploitation lorsqu'il doit déterminer ce qu'il faut faire ensuite. La planification est un algorithme distribué. Pour mieux comprendre les machines multicœurs, imaginez que chaque noyau exécute le noyau séparément. Tout comme un programme multi-thread, le noyau est écrit pour que son code sur un noyau puisse interagir en toute sécurité avec le code sur d'autres cores pour mettre à jour des structures de données partagées (comme la liste des threads prêts à être exécutés).
Quoi qu'il en soit, le système d'exploitation aide les processus multi-threadés à exploiter le parallélisme au niveau des threads, ce qui doit être explicitement exposé en écrivant manuellement un programme multi-thread . (Ou par un compilateur à parallélisation automatique avec OpenMP ou quelque chose).
Ensuite, la partie frontale de la CPU organise ces instructions en distribuant un thread à chaque cœur et distribue des instructions indépendantes de chaque thread parmi tous les cycles ouverts.
Un cœur de processeur n'exécute qu'un flux d'instructions s'il n'est pas arrêté (endormi jusqu'à la prochaine interruption, par exemple une interruption du minuteur). C'est souvent un thread, mais il peut également s'agir d'un gestionnaire d'interruption du noyau ou de divers codes de noyau si le noyau décide de faire autre chose que de simplement revenir au thread précédent après le traitement 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 d'un point de vue de 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 essaye de programmer les threads pour séparer les cœurs physiques afin de ne pas t rivaliser les uns avec les autres. 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étectera que 4).
Le terme " front-end" 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 émet dans la partie en panne du cœur . Chaque noyau a son propre front-end et fait partie du noyau dans son ensemble. Les instructions qu'il récupère correspondent à ce que le processeur 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êtes et qu'il existe un port d'exécution libre. Cela ne doit pas nécessairement se produire dans l'ordre des programmes. C'est ainsi qu'un processeur OOO peut exploiter le parallélisme au niveau des instructions au sein d'un seul thread .
Si vous remplacez "noyau" par "unité d'exécution" dans votre idée, vous êtes sur le point de corriger. Oui, la CPU distribue des instructions / unités indépendantes aux unités d'exécution en parallèle. (Mais il y a une confusion dans la terminologie, puisque vous avez dit "front-end" alors que c'est en réalité le programmateur d'instructions du processeur, appelé Reservation Station, qui sélectionne les instructions prêtes à être exécutées).
Une exécution dans le désordre ne peut trouver ILP qu’à un niveau très local, jusqu’à quelques centaines d’instructions, et non entre deux boucles indépendantes (à moins qu’elles ne soient courtes).
Par exemple, l’équivalent asm de cette
int i=0,j=0;
do {
i++;
j++;
} while(42);
fonctionnera à peu près aussi vite que la même boucle, incrémentant d’un seul compteur sur Intel Haswell. i++
dépend uniquement de la valeur précédente de i
, alors que j++
ne dépend que de la valeur précédente de j
, de sorte que les deux chaînes de dépendance puissent s'exécuter en parallèle sans rompre l'illusion que tout soit 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 chacun d’eux est additionné, de sorte qu’il peut supporter un débit allant jusqu’à 4 inc
instructions par horloge s’ils sont tous indépendants. (Avec une latence = 1, il vous suffit donc de 4 registres pour maximiser le débit en conservant 4 inc
instructions en vol. Contrastez ceci avec le vecteur-FP MUL ou FMA: latence = 5 débits = 0.5 nécessite 10 accumulateurs vectoriels pour garder 10 FMA en vol. maximum, et chaque vecteur peut être 256b, contenant 8 flotteurs simple précision).
La branche prise est également un goulet d'étranglement: une boucle prend toujours au moins une horloge complète par itération, car le débit de la branche prise est limité à 1 par horloge. Je pourrais insérer une instruction supplémentaire dans la boucle sans réduire les performances, sauf si elle lit / écrit également eax
ou, edx
dans ce cas, cela allongerait la chaîne de dépendance. Le fait de placer 2 instructions supplémentaires dans la boucle (ou une instruction complexe comportant plusieurs étapes) créerait un goulot d'étranglement au niveau du front-end, car il ne peut émettre que 4 uops par horloge dans le cœur en panne. (Voir ce SO Q & A pour des 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 des cas plus complexes, trouver le parallélisme nécessite de regarder une fenêtre d'instructions plus grande . (par exemple, il existe peut-être une séquence de 10 instructions qui dépendent toutes les unes des autres, puis de quelques instructions indépendantes).
La capacité de la mémoire tampon de réapprovisionnement est l’un des facteurs limitant la taille de la fenêtre hors d’ordre. Sur Intel Haswell, c'est 192 oups. (Et vous pouvez même le mesurer expérimentalement , ainsi que la capacité de changement de nom de registre (taille du fichier de registre).) Les cœurs de processeur à faible consommation, tels que ARM, ont des tailles de ROB beaucoup plus petites, si elles exécutent du tout dans le désordre.
Notez également que les processeurs doivent être en pipeline, mais également en panne. Il doit donc extraire 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 manqué les cycles d'extraction. Les succursales sont délicates, car nous ne savons même pas où aller chercher si nous ne savons pas comment une succursale est allée. C'est pourquoi la prédiction de branche est si importante. (Et pourquoi les processeurs modernes ont recours à une exécution spéculative: ils déterminent le sens dans lequel une branche ira et commenceront à extraire / 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, vous trouverez des liens dans le wiki des balises Stackoverflow x86 , y compris vers le guide microarch d'Agner Fog et vers les écritures détaillées de David Kanter avec des diagrammes de processeurs Intel et AMD. D'après sa description de la microarchitecture d'Intel Haswell , il s'agit du diagramme final de l'ensemble du pipeline d'un noyau Haswell (et non de la puce entière).
Ceci est un schéma fonctionnel d'un seul cœur de processeur . Une CPU quad-core en a 4 sur une puce, chacune avec ses propres caches L1 / L2 (partage d'un cache L3, de contrôleurs de mémoire et de connexions PCIe avec les périphériques système).
Je sais que c'est extrêmement compliqué. L'article de Kanter montre également certaines parties de cette discussion pour parler de l'interface séparément des unités d'exécution ou des caches, par exemple.