# 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 ordinateurs, qui exécutent chacun un ou plusieurs programmes dans leur coin. Si utiliser plusieurs ordinateurs connectés par un réseau local est une solution simple et facile à implémenter, elle est cependant assez onéreuse et demande des logiciels adaptés qui permettent de profiter d'une telle architecture matérielle. D'autres solutions multiprocesseurs ont alors vu le jour pour rendre l'usage de plusieurs processeurs plus adéquat.

## Les systèmes multiprocesseur

Les systèmes multiprocesseur placent plusieurs processeurs sur la même carte mère. Cette méthode marchait bien mais n'était pas des plus pratique, surtout que l'utilisation de plusieurs processeurs en même temps n'était de plus pas vraiment démocratisé à cette époque. Les logiciels et les systèmes d'exploitation grand public n'étaient pas adaptés pour, la demande pour des ordinateurs multiprocesseurs était limitée à quelques professionnels et ne justifiait pas un investissement majeure dans ces technologies.
Au niveau matériel, il fallait une carte mère adaptée, qui n'était pas facilement disponible et coûtait généralement plus cher que les cartes mères à un seul processeur. Sans compter qu'il valait mieux avoir une mémoire RAM rapide et des processeurs dotés de beaucoup de mémoire cache, ce qui coûtait encore plus cher. Au final, le gain était assez faible en terme de performance, peu de logiciels grand public profitaient de l'usage de plusieurs processeurs, la technologie est restée confidentielle.

### Le réseau d'interconnexion inter-processeur

Un système multi-processeurs doit connecter entre eux plusieurs processeurs. Pour cela, les cartes mères multi-processeurs incorporent un réseau d'interconnexion pour connecter les processeurs entre eux, et les connecter avec la mémoire et les autres entrées-sorties. Il s'agit d'un réseau d'interconnexion inter-processeur, placé sur la carte mère. Les processeurs sont tous connectés en utilisant deux solutions : soit avec un bus partagé, soit avec un réseau d'interconnexion très complexe.
Une solution simple relie les processeurs/cœurs entre eux grâce à un bus partagé. Nous parlerons en détail du bus partagé dans le chapitre sur la cohérence des caches, mais nous pouvons aborder le sujet maintenant, en précisant quel est ce bus partagé. Et suivant que l'on parle de systèmes multi-processeurs ou multicœurs, le bus qui est partagé n'est pas le même. Le cas le plus simple est celui d'une architecture multi-processeurs. Les processeurs sont alors tous reliés à la mémoire RAM à travers le bus mémoire. Le bus mémoire sert de point de ralliement, de point de convergence, il est le bus partagé. Les transferts entre caches se font alors en utilisant le bus mémoire.
De nos jours, les cartes mères pour serveur n'utilisent plus de bus partagé, mais utilisent un réseau d'interconnexion plus élaboré. Le réseau en question est un vrai réseau, qui peut être un réseau en anneau, un réseau crossbar, un réseau routé, etc. Le réseau d'interconnexion relie entre eux les cœurs, l'interface avec la mémoire RAM, le cache partagé L3/L4, mais aussi le bus PCI-Express, le GPU et quelques autres circuits séparés.
Les systèmes multi-processeurs 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 remplacent le fameux front-side bus des tout premiers processeurs multicœurs.
Un exemple 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.
| AMD Epyc 7702. | Schéma fonctionnel de l'AMD Epyc. |

### Les interruptions inter-processeurs

Les différents processeurs sont gérés par le système d'exploitation de l'ordinateur, avec l'aide d'interruptions inter-processeurs, des interruptions déclenchées sur un processeur et exécutées sur un autre, éventuellement par tous les autres pour certaines interruptions spécifiques. 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.
Un exemple d'implémentation très simple est celui de la console Néo-géo, qui contenait deux processeurs. Le premier est un Motorola 68000 qui sert de processeur principal, l'autre est un Z80 qui sert de processeur dédié à l'audio. Le Z80 commande un synthétiseur sonore, et est relié à sa propre mémoire, distincte de la mémoire principale. Le MC68000 est le processeur principal et a une relation maitre-esclave avec le Z80 : le MC68000 envoie des interruptions au Z80, mais la communication ne va pas dans l'autre sens.
Les deux processeurs communiquent via l'intermédiaire d'un IO arbiter chip, un circuit sur la carte mère connecté ua bus, qui gère les interruptions inter-processeur. Il contient un registre de 8 bits, dans lequel le MC68000 peut écrire dedans à sa guise, le registre étant adressable par le processeur. Les valeurs écrites dans ce registre sont des numéros d'interruption, qui indiquent quelle routine d'interruption exécuter. Lorsque le MC68000 écrit une valeur dedans, cela déclenche l’exécution automatique d'une interruption sur le Z80. Le code de 8 bits est envoyé au processeur Z80, en parallèle de l'interruption.
    Il est possible de voir ce IO arbiter chip comme un contrôleur d'interruptions spécialisé dans les interruptions inter-processeur.
Les anciens PC incorporaient sur leur carte mère un contrôleur d'interruption créé par Intel, le 8259A, qui ne gérait pas les interruptions inter-processeurs. Les cartes mères multiprocesseurs devaient incorporer un contrôleur spécial en complément. 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.
On peut préciser le processeur de destination en configurant certains registres du IO-APIC, afin que celui-ci redirige la demande d'interruption d'un local APIC vers celui sélectionné. Cela se fait avec l'aide d'un registre de 64 bits nommé l'Interrupt Command Register. Pour simplifier le mécanisme complet, chaque processeur se voit attribuer un Id au démarrage qui permet de l'identifier (en fait, cet Id est attribué au local APIC de chaque processeur). Certains bits de ce registre permettent de préciser quel est le type de transfert : doit-on envoyer l'interruption au processeur émetteur, à tous les autres processeurs, à un processeur particulier. Dans le dernier cas, certains bits du registre permettent de préciser l'Id du processeur qui va devoir recevoir l'interruption. À charge de l'APIC de faire ce qu'il faut en fonction du contenu de ce registre.

## Les processeurs multicœurs

Puis, avec les progrès en matière de miniaturisation des processeurs, les fabricants ont eu l'idée d'utiliser les transistors qu'ils avaient pour fabriquer des processeurs multicœurs, un terme que vous avez peut-être déjà entendu sans vraiment comprendre ce qu'il signifiait réellement. Les processeurs multicœurs peuvent être vus comme un regroupement de plusieurs processeurs sur la même puce de silicium.
Pour être plus précis, ils contiennent plusieurs cœurs, chaque cœur pouvant exécuter un programme tout seul. Chaque 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 d'un processeur ne sont présents qu'en un seul exemplaire dans un processeur multicœurs, comme les circuits de communication avec la mémoire ou les circuits d’interfaçage avec la carte mère. 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. Ces processeurs sont devenus la norme dans les ordinateurs grand public et les logiciels et systèmes d'exploitation se sont adaptés.
Dans ce qui suit, nous utiliserons le terme cœurs pour désigner soit les cœurs d'un processeur multicœurs, soit les différents processeurs d'un ordinateur multiprocesseur. Tout le contenu de ce chapitre vaut aussi bien pour les systèmes multicœurs que multiprocesseur. Les différences entre les deux sont mineures, les deux font face aux mêmes problèmes, les mêmes solutions peuvent s'appliquer sur les deux types de systèmes.

### Le multicœurs symétrique ou asymétrique

Dans la grosse majorité des cas, les cœurs d'un processeur multicœurs sont tous identiques. Mais ce n'est certainement pas une obligation et on peut très bien mettre plusieurs cœurs assez différents sur la même puce. On peut par exemple utiliser 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.
Un exemple est celui du processeur CELL de la console de jeu PS3. Il intègre un cœur principal POWER PC v5 et 8 coeurs 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. 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.

### Les architectures à cœurs conjoints

Sur certains processeurs multicœurs, certains circuits sont partagés entre plusieurs cœurs. Cette technique consistant à ne pas dupliquer certains circuits et à en partager certains s'appelle le cluster multithreading, ou encore les architectures à cœurs conjoints (Conjoined Core Architectures). Typiquement, les mémoires caches et l'unité de calcul flottantes peuvent être partagées sans trop de problèmes, les unités SIMD qu'on verra dans quelques chapitres sont aussi dans ce cas.
Elle est notamment utilisée sur les processeurs FX-8150 et FX-8120 d'AMD, et les autres processeurs basés sur l'architecture Bulldozer. Sur ces processeurs, tous les cœurs se partagent l'unité de calcul sur les nombres flottants. 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.

### Les architectures many core

Les architectures many core ont un très grand nombre de cœurs, plus d'une cinquantaine, voire plusieurs centaines ou milliers. La différence est surtout une question de degré, mais aussi de but recherché.
Les architectures multicœurs sont surtout conçues pour les ordinateurs personnels, éventuellement les serveurs. Elles recherchent un bon compromis entre un grand nombre de cœurs, et une bonne performance pour les programmes non-parallèles. En clair, elles évitent de sacrifier les performances pour les applications non-parallèles, ce qui fait que leurs cœurs sont généralement très puissants, avec beaucoup d'optimisations micro-architecturales.
Les architectures many core font le compromis inverse : elles ont beaucoup de cœurs, mais ceux-ci ne sont pas très puissants, surtout pour les applications non-parallèles. Les cœurs des architectures many core sont généralement des cœurs sans exécution dans le désordre, sans prédiction de branchements, sans renommage de registres, voire sans pipeline ni parallélisme d'instruction.

## Le partage des caches

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

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.
| Caches dédiés | Cache partagé |
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. A 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

Dans la réalité, un processeur multicœur ne contient pas qu'un seul cache. Les processeurs actuels ont une organisation hybride, dans laquelle certains caches sont partagés et pas d'autres. Le cache L1 n'est jamais partagé, car ce cache doit avoir une latence très faible, ce qui augmente les temps d'accès. Partager le cache L1 serait parfaitement possible, mais rendrait celui-ci trop lent pour sa tâche. 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é. Il fallait relier le cache L2 partagé aux nombreux caches L1, ce qui était fait par 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.
De nos jours, les processeurs ont ajouté des caches L3 et même L4, de grande capacité. Le partage des caches s'est alors complexifié. Pour le cache L3 et le cache L4, il est systématiquement partagé, car son rôle est d'être un cache lent mais gros, ce qui colle parfaitement à ce qu'on attend d'un cache partagé. Le cas du cache L2 est un peu à part. Pour le cache L2, cela dépend des architectures : il est partagé sur certains processeurs, dédié sur d'autres.
Dans le cas le plus courant, il y a plusieurs caches L2 qui sont chacun partagés 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

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

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

Les CPU multicœurs connectent plusieurs cœurs entre eux, tout comme les systèmes multi-processeurs connectent plusieurs processeurs entre eux. Mais les transferts de données entre cœurs se font par l'intermédiaire des caches, ce qui fait que l'implémentation est différente de celle utilisée sur les systèmes multi-processeurs. Les standards pour les interconnexions entre coeurs sont assez proches de ceux utilisés pour interconnecter des processeurs, mais les standards utilisés ne sont pas les mêmes en pratique. Là encore, les cœurs sont connectés entre eux soit par un bus, soit par un réseau d'interconnexion.

### Le bus partagé entre plusieurs cœurs

Une solution simple relie les cœurs entre eux grâce à un bus partagé. Le cas le plus simple est celui où chaque cœur dispose de caches dédiés, qui sont alors tous reliés au bus mémoire, qui sert d'intermédiaire. Mais l'idée doit être adaptée sur les processeurs multicœurs avec 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.

### Le réseau d'interconnexion entre plusieurs cœurs

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.
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

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.