Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs
Pour réellement tirer parti du parallélisme de taches, rien ne vaut l'utilisation de plusieurs processeurs et/ou de plusieurs cœurs, qui exécutent chacun un ou plusieurs programmes dans leur coin. Des solutions multiprocesseurs ont alors vu le jour pour rendre l'usage de plusieurs processeurs plus adéquat. Avant de poursuivre, nous allons voir les systèmes multiprocesseur à part des processeurs multicœurs. Il faut dire qu'utiliser plusieurs processeurs et avoir plusieurs cœurs sur la même puce, ce n'est pas la même chose. Particulièrement pour ce qui est de la mémoire cache.
Les systèmes multiprocesseur placent plusieurs processeurs sur la même carte mère. Ils sont courants dans les serveurs ou les data centers, mais sont beaucoup plus rares pour les ordinateurs grand public. Il y a eu quelques systèmes multiprocesseur vendus au grand public dans les années 2000, certaines cartes mères avaient deux sockets pour mettre deux processeurs. Mais les logiciels et les systèmes d'exploitation grand public n'étaient pas adaptés pour, ce qui fait que la technologie est restée confidentielle.
Puis, en 2005, les processeurs multicœurs sont arrivés. Ils peuvent être vus comme un regroupement de plusieurs processeurs dans le même circuit intégré. Pour être plus précis, ils contiennent plusieurs cœurs, chaque cœur pouvant exécuter un programme tout seul. Un cœur dispose de toute la machinerie électronique pour exécuter un programme, que ce soit un séquenceur d'instruction, des registres, une unité de calcul. Par contre, certains circuits du processeur sont partagés entre les cœurs, comme les circuits d’interfaçage avec la mémoire.
Les processeurs multicœurs sont devenus la norme dans les ordinateurs grand public et les logiciels et systèmes d'exploitation se sont adaptés. Suivant le nombre de cœurs présents dans notre processeur, celui-ci sera appelé un processeur double-cœur (deux cœurs), quadruple-cœur (4 cœurs), octuple-cœur (8 cœurs), etc. Les processeurs grand public ont généralement entre 8 et 16 cœurs, à l'heure où j'écris ces lignes (2025), rarement au-delà. Par contre, les processeurs pour serveurs dépassent la vingtaine de cœurs. Les serveurs utilisent souvent des architectures dites many core, qui ont un très grand nombre de cœurs, plus d'une cinquantaine, voire plusieurs centaines ou milliers.
- Dans ce qui suit, nous utiliserons les termes "processeurs" et "cœurs" comme s'ils étaient synonymes. Tout ce qui vaut pour les systèmes multiprocesseur vaut aussi pour les systèmes multicœurs.
Le partage des caches
[modifier | modifier le wikicode]Quand on conçoit un processeur multicœur, il ne faut pas oublier ce qui arrive à la pièce maîtresse de tout processeur actuel : le cache ! Pour le moment nous allons oublier le fait que les processeurs ont une hiérarchie de caches, avec des caches L1, L2, L3 et autres. Nous allons partir du principe qu'un processeur simple cœur a un seul cache, et voir comment adapter le cache à la présence de plusieurs cœurs. Nous allons rapidement lever cette hypothèse, pour étudier le cas où un processeur multicœur a une hiérarchie de caches, mais seulement après avoir vu le cas le plus simple à un seul cache.
Le partage des caches sans hiérarchie de caches : caches dédiés et partagés
[modifier | modifier le wikicode]Avec un seul niveau de cache, sans hiérarchie, deux solutions sont possibles. La première consiste à garder un seul cache, et de le partager entre les cœurs. L'autre solution est de dupliquer le cache et d'utiliser un cache par cœur. Les deux solutions sont appelées différemment. On parle de caches dédiés si chaque cœur possède son propre cache, et de cache partagé avec un cache partagé entre tous les cœurs. Ces deux méthodes ont des inconvénients et des avantages.
Le premier point sur lequel comparer caches dédiés et partagés est celui de la capacité du cache. La quantité de mémoire cache que l'on peut placer dans un processeur est limitée, car le cache prend beaucoup de place, près de la moitié des circuits du processeur. Aussi, un processeur incorpore une certaine quantité de mémoire cache, qu'il faut répartir entre un ou plusieurs caches. Les caches dédiés et partagés ne donnent pas le même résultat. D'un côté, le cache partagé fait que toute la mémoire cache est dédiée au cache partagé, qui est très gros. De l'autre, on doit répartir la capacité du cache entre plusieurs caches séparés, individuellement plus petits. En conséquence, on a le choix entre un petit cache pour chaque processeur ou un gros cache partagé.
Le choix entre les deux n'est pas simple, mais doit tenir compte du fait que les programmes exécutés sur les cœurs n'ont pas les mêmes besoins. Certains programmes sont plus gourmands et demandent beaucoup de cache, alors que d'autres utilisent peu la mémoire cache. Avec un cache dédié, tous les programmes ont accès à la même quantité de cache, car les caches des différents cœurs sont de la même taille. Les caches dédiés étant assez petits, les programmes plus gourmands devront se débrouiller avec un petit cache, alors que les autres programmes auront du cache en trop. À l'opposé, un cache partagé répartit le cache de manière optimale : un programme gourmand peut utiliser autant de cache qu'il veut, laissant juste ce qu'il faut aux programmes moins gourmands. le cache peut être répartit plus facilement selon les besoins des différents programmes.

Un autre avantage des caches partagés est quand plusieurs cœurs accèdent aux même données. C'est un cas très courant, souvent lié à l'usage de mémoire partagé ou de threads. Avec des caches dédiés, chaque cœur a une copie des données partagées. Mais avec un cache partagé, il n'y a qu'une seule copie de chaque donnée, ce qui utilise moins de mémoire cache. Imaginons que l'on sait 8 caches dédiés de 8 Kibioctets, soit 64 kibioctets au total, comparé à un cache partagé de même capacité totale. Les doublons dans les caches dédiés réduiront la capacité mémoire utile, effective, comparé à un cache partagé. S'il y a 1 Kibioctet de mémoire partagé, 8 kibioctets seront utilisés pour stocker ces données en doublons, seulement 1 kibioctet sur un cache partagé. Ajoutons aussi que la cohérence des caches est grandement simplifiée avec l'usage d'un cache partagé, vu que les données ne sont pas dupliquées dans plusieurs caches.
Mais le partage du cache peut se transformer en inconvénient si les programmes entrent en compétition pour le cache, que ce soit pour y placer des données ou pour les accès mémoire. Deux programmes peuvent vouloir accéder au cache en même temps, voire carrément se marcher sur les pieds. La résolution des conflits d'accès au cache est résolu soit en prenant un cache multiport, avec un port dédié par cœur, soit par des mécanismes d'arbitrages avec des circuits dédiés. Le revers de la médaille tient au temps de latence. Plus un cache est gros, plus il est lent. En conséquence, des caches dédiés seront plus rapides qu'un gros cache partagé plus lent.
Le partage des caches adapté à une hiérarchie de caches
[modifier | modifier le wikicode]Dans la réalité, un processeur multicœur ne contient pas qu'un seul cache, mais une hiérarchie de caches avec des caches L1, L2 et L3, parfois L4. Dans cette hiérarchie, certains caches sont partagés entre plusieurs cœurs, les autres sont dédiés. Le cache L1 n'est jamais partagé, car il doit avoir un temps d'accès très faible. Pour les autres caches, tout dépend du processeur.

Les premiers processeurs multicœurs commerciaux utilisaient deux niveaux de cache : des caches L1 dédiés et un cache L2 partagé. Le cache L2 partagé était relié aux caches L1, grâce à un système assez complexe d'interconnexions. Le cache de niveau L2 était souvent simple port, car les caches L1 se chargent de filtrer les accès aux caches de niveau inférieurs.
Les processeurs multicœurs modernes ont des caches L3 et même L4, de grande capacité, ce qui a modifié le partage des caches. Le cache de dernier niveau, à savoir le cache le plus proche de la mémoire, est systématiquement partagé, car son rôle est d'être un cache lent mais gros. Il s'agit le plus souvent d'un cache de L3, plus rarement L4. Sur certains processeurs multicœurs, le cache de dernier niveau n'est techniquement pas dans le cœur, mais fait partie d'un ensemble de circuits reliés, comme le contrôleur mémoire ou l'interface mémoire. Il fonctionne à une fréquence différente du processeur, n'a pas la même tension d'alimentation, etc.
Le cas du cache L2 dépend des architectures : il est partagé sur certains processeurs, dédié sur d'autres. Mais sur les processeurs modernes, c'est un cache dédié soit par cœur, soit pour un groupe de cœurs. Dans le cas le plus courant, chaque cache L2 est partagé entre plusieurs cœurs mais pas à tous. En effet, on peut limiter le partage du cache à quelques cœurs particuliers pour des raisons de performances.

D'autres processeurs ont des caches L2 dédiés. Il s'agit surtout des processeurs multicœurs anciens, parmi les premières générations de processeurs multicœurs. Un exemple est celui de la microarchitecture Nehalem d'Intel. Il avait des caches L1 et L2 dédiés, mais un cache L3 partagé.

Les caches partagés centralisés et distribués
[modifier | modifier le wikicode]Un point important est que quand on parle de cache partagé ou de cache dédié, on ne parle que de la manière dont les cœurs peuvent accéder au cache, pas de la manière dont le caches est réellement localisé sur la puce. En théorie, qui dit plusieurs caches dédiés signifie que l'on a vraiment plusieurs caches séparés sur la puce. Et chaque cache dédié est proche du cœur qui lui est attribué. Et pour les caches partagés unique, une portion de la puce de silicium contient le cache, que cette portion est un énorme bloc de transistors. Il est généralement placé au milieu de la puce ou sur un côté, histoire de facilement le connecter à tous les cœurs.
Mais pour les caches séparés, ce n'est pas toujours le cas. Avoir un cache énorme poserait des problèmes sur les architectures avec beaucoup de cœurs. En réalité, le cache est souvent découpé en plusieurs banques, reliées à un contrôleur du cache par un système d'interconnexion assez complexe. Les banques sont physiquement séparées, et il arrive qu'elles soient placées proche d'un cœur chacune. L'organisation des banques ressemble beaucoup à l'organisation des caches dédiés, avec une banque étant l'équivalent d'un cache dédié. La différence est que les cœurs peuvent lire et écrire dans toutes les banques, grâce au système d'interconnexion et au contrôleur de cache.
Tel était le cas sur les processeurs AMD Jaguar. Ils avaient un cache L2 de 2 mébioctets, partagés entre tous les cœurs, qui était composé de 4 banques de 512 Kibioctets. Les quatre banques du cache étaient reliées aux 4 cœurs par un réseaux d'interconnexion assez complexe.

La différence entre les deux solutions pour les caches partagés porte le nom de cache centralisés versus distribués. Un gros cache unique sur la puce est un cache centralisé, et c'est généralement un cache partagé. Mais un cache composé de plusieurs banques dispersées sur la puce est un cache distribué, qui peut être aussi bien dédié que partagé.
Les caches virtualisés
[modifier | modifier le wikicode]Il faut noter que quelques processeurs utilisent cette technique pour fusionnent le cache L2 et le cache L3. Par exemple, les processeurs IBM Telum utilisent des caches L3 virtualisés, dans leurs versions récentes. Le processeur Telum 2 contient 10 caches L2 de 36 mébioctets chacun, soit 360 mébioctets de cache. L'idée est que ces 360 mébioctets sont partagés à la demande entre cache L2 dédié et cache L3. On parle alors de cache virtualisé.
Un cache de 36 mébioctet est associé à un cœur, auquel il est directement relié. Les cœurs n'utilisent pas tous leur cache dédié à 100% Il arrive que des cœurs aient des caches partiellement vides, alors que d'autres on un cache qui déborde. L'idée est que si un cœur a un cache plein, les données évincées du cache L2 privé sont déplacées dans le cache L2 d'un autre cœur, qui lui est partiellement vide. Le cache L2 en question est alors partitionné en deux : une portion pour les données associée à son cœur, une portion pour les données des L2 des autres cœurs.
Pour que la technique fonctionne, le processeur mesure le remplissage de chaque cache L2. De plus, il faut gérer la politique de remplacement des lignes de cache. Une ligne de cache évincée du cache doit être déplacé dans un autre L2, pas dans les niveaux de cache inférieur, ni dans la mémoire. Du moins, tant qu'il reste de la place dans le cache L3. De plus, une lecture/écriture dans le cache L3 demande de localiser le cache L2 contenant la donnée. Pour cela, les caches L2 sont tous consultés lors d'un accès au L3, c'est la solution la plus simple, elle marche très bien si le taux de défaut du cache L2 est faible.
Une telle optimisation ressemble beaucoup à un cache L2/L3 distribué, mais il y a quelques différences qui sont décrites dans le paragraphe précédent. Avec un L2 distribué, tout accès au L2 déclencherait une consultation de toutes les banques du L2. Avec un cache L3 virtualisé, ce n'est pas le cas. Le cache L2 associé au cœur est consulté, et c'est seulement en cas de défaut de cache que les autres caches L2 sont consultés. De plus, avec un cache L2 distribué, il n'y a pas de déplacement d'une ligne de cache entre deux banques, entre deux caches L2 physiques. Alors qu'avec un cache L3 virtualisé, c'est le cas en cas de remplacement d'une ligne de cache dans le cache L2.
Sur le processeur Telum 1, le partage du cache L2 est assez simple. Un cache L2 fait 32 mébioctets et est découpé en deux banques de 16 mébioctets. En temps normal, les premiers 16 mébioctets sont toujours associé au cache L2, au cœur associé. Les 16 mébioctets restants peuvent soit être attribués au cache L3, soit fusionnés avec les 16 premiers mébioctets. Dans le cas où le cœur associé est en veille, n'est absolument pas utilisé, les 32 mébioctets sont attribués au cache L3. Un partage assez simple, donc. Le partage du cache L2/L3 sur les processeurs Telum 2 n'est pas connu, il est supposé être plus flexible.
Le réseau d'interconnexion entre cœurs
[modifier | modifier le wikicode]Les systèmes avec plusieurs processeurs incorporent un réseau d'interconnexion pour connecter les processeurs entre eux, ainsi qu'à la mémoire RAM. Il s'agit d'un réseau d'interconnexion inter-processeur, placé sur la carte mère. Les CPU multicœurs ont aussi un tel réseau d'interconnexion, pour relier les cœurs entre eux. La différence est que le réseau d'interconnexion est placé dans le processeur, pas sur la carte mère.
Les systèmes multi-cœurs modernes utilisent des réseaux d'interconnexion standardisés, les standards les plus communs étant l'HyperTransport, l'Intel QuickPath Interconnect, l'IBM Elastic Interface, le Intel Ultra Path Interconnect, l'Infinity Fabric, etc. Ils sont aussi utilisés pour faire communiquer entre eux plusieurs processeurs.

Le bus partagé entre plusieurs cœurs
[modifier | modifier le wikicode]Pour un faible nombre de coeurs/processeurs, la solution utilisée relie les processeurs entre eux grâce au bus mémoire. Le bus mémoire est donc un bus partagé, avec tout ce que cela implique.

Pour les systèmes multicœurs, l'usage d'un bus partagé doit être adaptée pour tenir compte des caches partagés. Voyons d'abord le cas d'un CPU avec deux niveaux de cache, dont un cache L2 est partagé entre tous les cœurs. Les caches L1 sont reliés au cache L2 partagé par un bus, qui n'a souvent pas de nom. Nous désignerons le bus entre le cache L1 et le cache L2 : bus partagé, sous-entendu partagé entre tous les caches. C'est lui qui sert à connecter les cœurs entre eux.

Un processeur multicœur typique a une architecture avec trois niveaux de cache (L1, L2 et L3), avec un niveau L1 dédié par cœur, un niveau L2 partiellement partagé et un L3 totalement partagé. Le bus partagé est alors difficile à décrire, mais il correspond à l'ensemble des bus qui connectent les caches L1 aux caches L2, et les caches L2 au cache L3. Il s'agit alors d'un ensemble de bus, plus que d'un bus partagé unique.
L'usage d'un bus partagé a cependant de nombreux défauts. Par exemple, les processeurs doivent se répartir l'accès au bus mémoire, il faut gérer le cas où deux processeurs accèdent au bus en même temps, etc. Pour cela, un composant dédié s'occupe de l'arbitrage entre processeurs. Il est généralement placé sur la carte mère de l'ordinateur, dans le chipset, dans le pont nord, ou un endroit proche. Les processeurs doivent être conçus pour communiquer avec ce circuit d'arbitrage, ou du moins avoir un support minimal pour le multiprocesseur.

D'autres défauts très importants seront abordés en détail dans le chapitre sur la cohérence des caches
Le réseau d'interconnexion entre plusieurs cœurs
[modifier | modifier le wikicode]Relier plusieurs cœurs avec des bus pose de nombreux problèmes techniques qui sont d'autant plus problématiques que le nombre de cœurs augmente. Le câblage est notamment très complexe, les contraintes électriques pour la transmission des signaux sont beaucoup plus fortes, les problèmes d'arbitrages se font plus fréquents, etc. Pour régler ces problèmes, les processeurs multicoeurs n'utilisent pas de bus partagé, mais un réseau d'interconnexion plus complexe.
Un exemple est le réseau du processeur Celle, utilisé sur la PS3. Les transferts sur ce bus se font par paquets de 128 bits. Le réseau était composé de 12 intermédiaires appelés Ramps. Chaque intermédiaire communiquait avec deux autres ramps ce qui formait un anneau de ramps. Les paquets de 128 bits circulaient sur cet anneau en passant d'un ramp au suivant, soit dans le sens horaires, soit dans le sens anti-horaire.
Avant d'entrer dans l'anneau, les processeurs envoient une requête à un circuit d'arbitrage, qui commande les transferts de données sur l'anneau. Si celui-ci accepte la requête, la donnée est envoyée au ramp et circule dans l'anneau. Il faut noter que le contrôleur mémoire a la priorité sur tout le reste. Le sens de transfert, horaire ou anti-horaire, est choisit de manière à prendre le chemin le plus court. Les paquets passent alors de ramp en ramp. Chaque ramp reçoit le paquet, regarde l'adresse de destination du paquet, et décide de la suite. Soit le paquet lui est destiné, il le traite. Sinon, le il transmet au ramp suivant.
Le bus permet de transférer 4 paquets de 128 bits par cycle. Pour cela, les ramps avaient quatre bus, chacun de 128 bits : deux bus pour le sens horaire, et deux autres bus pour le sens anti-horaire. Il peut y avoir 3 transferts simultanés, tant que les paquets ne se recouvrent pas, mais les transferts sont de taille arbitraire. Le réseau d'interconnexion avait une fréquence de 1,6 GHz. Si vous faites les calculs, vous tomberez cependant sur un débit binaire théorique d'environ 300 gibioctets par secondes, qui n'est cependant jamais atteint en pratique, pour des raisons techniques liées à la cohérence des caches. La limite théorique est en pratique plus proche des 200 gibioctets par secondes, en tenant compte de ces limitations théoriques.

Un autre xemple de réseau d'interconnexion est celui des architectures AMD EPYC, de microarchitecture Zen 1. Elles utilisaient des chiplets, à savoir que le processeur était composé de plusieurs puces interconnectées entre elles. Chaque puce contenait un processeur multicoeurs intégrant un cache L3, avec un réseau d'interconnexion interne au processeur sans doute basé sur un ensemble de bus. De plus, les puces étaient reliées à une puce d'interconnexion qui servait à la fois d'interface entre les processeurs, mais aussi d'interface avec la R1AM, le bus PCI-Express, etc. La puce d'interconnexion était gravée en 14 nm contre 7nm pour les chiplets des cœurs.
Le réseau d'interconnexion peut être très complexe, avec des connexions réseau, des commutateurs, et des protocoles d'échanges entre processeurs assez complexes basés sur du passage de messages. De telles puces utilisent un réseau sur puce (network on chip). Mais d'autres simplifient le réseau d'interconnexion, qui se résume à un réseau crossbar, voire à des mémoires FIFO pour faire l'interface entre les cœurs.
Le problème principal des réseaux sur puce est que les mémoires FIFOs sont difficiles à implémenter sur une puce de silicium. Elles prennent beaucoup de place, utilisent beaucoup de portes logiques, consomment beaucoup d'énergie, sont difficiles à concevoir pour diverses raisons (les accès concurrents/simultanés sont fréquents et font mauvais ménage avec les timings serrés de quelques cycles d'horloges requis). Il est donc impossible de placer beaucoup de mémoires FIFO dans un processeur, ce qui fait que les commutateur sont réduits à leur strict minimum : un réseau d'interconnexion, un système d'arbitrage simple parfois sans aucune FIFO, guère plus.
Les architectures en tile
[modifier | modifier le wikicode]Un cas particulier de réseau sur puce est celui des architectures en tile, des architectures avec un grand nombre de cœurs, connectés les unes aux autres par un réseau d'interconnexion "rectangulaire". Chaque cœur est associé à un commutateur (switch) qui le connecte au réseau d'interconnexion, l'ensemble formant une tile.

Le réseau est souvent organisé en tableau, chaque tile étant connectée à plusieurs voisines. Dans le cas le plus fréquent, chaque tile est connectée à quatre voisines : celle du dessus, celle du dessous, celle de gauche et celle de droite. Précisons que cette architecture n'est pas une architecture distribuée dont tous les processeurs seraient placés sur la même puce de silicium. En effet, la comparaison ne marche pas pour ce qui est de la mémoire : tous les cœurs accèdent à une mémoire partagée située en dehors de la puce de silicium. Le réseau ne connecte pas plusieurs ordinateurs séparés avec chacun leur propre mémoire, mais plusieurs cœurs qui accèdent à une mémoire partagée.
Un bon exemple d'architecture en tile serait les déclinaisons de l'architecture Tilera. Les schémas du-dessous montrent l'architecture du processeur Tile 64. Outre les tiles, qui sont les éléments de calcul de l'architecture, on trouve plusieurs contrôleurs mémoire DDR, divers interfaces réseau, des interfaces série et parallèles, et d'autres entrées-sorties.

Les interruptions inter-processeurs
[modifier | modifier le wikicode]Les interruptions inter-processeurs sont des interruptions déclenchées sur un processeur et exécutées sur un autre. Elles sont très utiles pour le système d'exploitation, pour des raisons qu'on ne peut pas expliquer ici. Disons simplement qu'elles permettent de répartir des programmes/threads sur plusieurs processeurs. Un programme/thread est démarré par une interruption, et le système d'exploitation détermine sur quel processeur elle doit être exécutée. L'utilité des interruptions inter-processeur est assez variée. Autrefois, elles servaient aussi pour la cohérence des caches, mais nous détaillerons cela dans un futur chapitre.
Une interruption inter-processeurs peut être envoyée soit à un cœur bien précis, soit à n'importe quel cœur, soit à tous les cœurs, voire même revenir à l'envoyeur. Tout dépend de ce que décide le système d'exploitation. Les trois situations ne sont pas identiques, sur un point : comment préciser quel est le processeur de destination ? Si on envoie une interruption à un cœur bien précis, il faut préciser quel est le cœur qui réceptionne l'interruption. Pour cela, il n'y a pas 36 solutions : on numérote les processeurs/cœurs avec un numéro de processeur. Ce numéro leur est soit attribué au démarrage par le BIOS, soit est gravé dans leur silicium pour les processeurs multicœurs.
Pour le reste, les interruptions inter-processeurs sont identiques aux interruptions normales. Elles ont un système de priorités, certaines devant passer avant les autres, là encore défini par des Interrupt Request Levels (IRQLs) ou quelque chose de similaire. Il peut y avoir des interruptions inter-processeur de type logicielles, à savoir lancées par une instruction machine. Par exemple, sur les IBM System/360 et les mainframes z/Architecture, le processeur avait une instruction SIGNAL PROCESSOR pour déclencher des interruptions logicielles inter-processeur.
Pour générer des interruptions inter-processeur, le contrôleur d'interruption doit pouvoir rediriger des interruptions déclenchées par un processeur vers un autre. Pour expliquer comment, nous allons étudier le cas des CPU x86, mais les implémentations ARM ou autres sont très similaires. L'ancien contrôleur d'interruption 8259A ne gérait pas les interruptions inter-processeurs, ce qui fait que les cartes mères multiprocesseurs devaient incorporer un contrôleur d'interruption spécial en complément. Par contre, son successeur, l'APIC, les gérait nativement.
De nos jours, chaque cœur x86 possède son propre contrôleur d’interruption, le local APIC, qui gère les interruptions en provenance ou arrivant vers ce processeur. On trouve aussi un IO-APIC, qui gère les interruptions en provenance des périphériques et de les redistribuer vers les APIC locaux. L'IO-APIC gère aussi les interruptions inter-processeurs en faisant passer les interruptions d'un local APIC vers un autre. Tous les APIC locaux et l'IO-APIC sont reliés ensembles par un bus APIC spécialisé, par lequel ils vont pouvoir communiquer et s'échanger des demandes d'interruptions.

Le déclenchement d'une interruption inter-processeur se fait en écrivant dans un registre appelé l'Interrupt Command Register. Un détail important est que l'écriture se fait dans le local APIC de l'envoyer, du processeur qui veut envoyer une interruption, pas dans le registre du processeur qui doit réceptionner l'interruption ! Le registre est composé de deux registres de 32 bits, et mémorise : le numéro du processeur de destination, le mode de transfert (à tous, à un cœur, etc), le numéro du vecteur d'interruption (pour préciser quelle interruption exécuter), et quelques informations supplémentaires. À charge de l'IO-APIC de faire ce qu'il faut en fonction du contenu de ce registre.
Le multiprocesseur/multicœur asymétrique
[modifier | modifier le wikicode]Sur les processeurs grand public actuels, les cœurs d'un processeur multicœurs sont tous identiques. Mais ce n'est certainement pas une obligation. On peut très bien regrouper plusieurs cœurs très différents, par exemple un cœur principal avec des cœurs plus spécialisés autour. Il faut ainsi distinguer le multicœurs symétrique, dans lequel on place des processeurs identiques sur la même puce de silicium, du multicœurs asymétrique où les cœurs ne sont pas identiques. Et il en est de même sur les systèmes avec plusieurs processeurs : on parle de multiprocesseur symétrique si les processeurs sont identiques, multiprocesseur asymétrique s'ils sont différents.
Précisons ce que nous entendons par "cœurs différents" ou "identiques". Les processeurs Intel modernes utilisent deux types de cœurs différents : des cœurs P et des cœurs E. Le P est pour Performance, le E est pour "Efficiency". Les deux ont le même jeu d'instruction : ce sont des processeurs x86. Par contre, ils ont des microarchitectures différentes. Et Intel n'est pas le seul à utiliser cette technique : ARM a fait pareil avec ses CPU d'architecture Big-little. Il n'est pas clair si de telles organisation sont du multicœur symétrique ou asymétrique. Le jeu d'instruction est identique, sauf éventuellement pour certaines extension comme l'AVX. Les deux coeurs n'ont pas les mêmes performances, mais est-ce suffisant ? La terminologie n'est pas claire.
Un exemple de multicoeurs asymétrique est celui du processeur CELL de la console de jeu PS3. Il était conçu spécifiquement pour cette console. Il intègre un cœur principal POWER PC v5 et 8 cœurs qui servent de processeurs auxiliaires. Le processeur principal est appelé le PPE et les processeurs auxiliaires sont les SPE. Les SPE sont reliés à une mémoire locale (local store) de 256 kibioctets qui communique avec le processeur principal via un bus spécial. Cette fois-ci, les coprocesseurs sont intégrés dans le même processeur.
Les SPE communiquent avec la RAM principale via des contrôleurs DMA. Les SPE possèdent des instructions permettant de commander leur contrôleur DMA et c'est le seul moyen qu'ils ont pour récupérer des informations depuis la mémoire. Et c'est au programmeur de gérer tout ça ! C'est le processeur principal qui va envoyer aux SPE les programmes qu'ils doivent exécuter. Il délègue des calculs aux SPE en écrivant dans le local store du SPE et en lui ordonnant l’exécution du programme qu'il vient d'écrire.

Annexe : les architectures à cœurs conjoints
[modifier | modifier le wikicode]Sur certains processeurs multicœurs, certains circuits sont partagés entre plusieurs cœurs. Typiquement, l'unité de calcul flottante est partagée entre deux coeurs/threads, les unités SIMD qu'on verra dans quelques chapitres sont aussi dans ce cas. Le partage de circuits permet d'éviter de dupliquer trop de circuits et donc d'économiser des transistors. Le problème est que ce partage est source de dépendances structurelles, ce qui peut entraîner des pertes de performances.
Cette technique consistant de partage d'unités de calcul entre coeurs s'appelle le cluster multithreading, ou encore les architectures à cœurs conjoints (Conjoined Core Architectures). Elle est notamment utilisée sur les processeurs AMD de microarchitecture Bulldozer, incluant ses trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Un exemple est celui des processeurs AMD FX-8150 et FX-8120.
Sur ces processeurs, les instructions sont chargées dans deux files d'instructions séparées, une par thread matériel. Les instructions sont ensuite décodées par un décodeur unique et renommées dans une unité de renommage unique. Par la suite, il y a deux voies entières séparées et une voie flottante partagée. Chaque voie entière a sa propre fenêtre d'instruction entière, son tampon de ré-ordonnancement, ses unités de calcul dédiées, ses registres, sa load-store queue, son cache L1. Par contre, la voie flottante partage les unités de calcul flottantes et n'a qu'une seule fenêtre d'instruction partagée par les deux threads.

La révision Steamroller sépara le front-end en deux voies distinctes, une par thread. Concrètement, elle ajouta un second décodeur d'instruction, une seconde file de micro-opération et une seconde unité de renommage de registres, afin d'améliorer les performances. Niveaux optimisations mineures, les stations de réservation ont été augmentées, elles peuvent mémoriser plus de micro-opérations, idem pour les bancs de registre et les files de lecture/écriture. Un cache de micro-opérations a été ajouté, de même que des optimisations quant au renommage de registre. Des ALU ont aussi été ajoutées, des FPU retirées.




