Vous pensez surement qu'il faut obligatoirement plusieurs cœurs pour exécuter plusieurs programmes en parallèle, mais sachez que c'est faux ! Il existe des processeurs spéciaux qui n'ont qu'un seul cœur, mais sont cependant capables d’exécuter plusieurs programmes quasi-simultanément. La différence avec les processeurs multicœurs est qu'ils ne sont pas capable d’exécuter plusieurs programmes en même temps, mais qu'il alternent entre les programmes à exécuter. Plusieurs programmes s’exécutent donc sur le même processeur, mais chacun à leur tour et non en même temps. D'ordinaire, cette alternance est gérée par le système d'exploitation, mais certains processeurs se passent de l'aide du système d'exploitation et gèrent cette alternance eux-même, directement au niveau matériel. Les processeurs en question sont appelés des processeurs multithreadés, ou encore des architectures multithreadées, pour les distinguer des processeurs multicœurs.
Du parallélisme avec un seul processeur.
Pour comprendre en quoi les architectures multithreadées sont intéressantes, il faut savoir qu'il arrive que l'unité de calcul d'un processeur ne fasse rien, pour diverses raisons. Par exemple, l'unité de calcul peut n'avoir aucun calcul à effectuer pendant que le processeur accède à la mémoire. Pour éviter cela, les différentes techniques d’exécution dans le désordre sont une solution toute indiquée, mais elles ne suffisent pas toujours. Les architectures multithreadées sont une autre solution à ce problème. Elles visent à exécuter plusieurs programmes sur un même processeur, de manière à ce que les temps morts d'un programme soient remplis par les calculs du second et réciproquement.
Notons que cette méthode marche d'autant mieux sur les processeurs sans exécution dans le désordre et autres optimisations du même genre. Architectures multithreadées et exécution dans le désordre ont en effet le même but, bien que les solutions apportées soient différentes. Il y a donc un conflit entre les deux approches. C'est la raison pour laquelle les processeurs grand public modernes, qui ont une exécution dans le désordre très performante, ne sont pas des architectures multithreadées.
Les registres sont dupliqués et le séquenceur adapté.
L'implémentation de ces architectures est assez simple. Partons du principe que le processeur est composé d'un séquenceur, d'un chemin de données et d'une fenêtre d'instruction. Le séquenceur charge et décode les instructions, le chemin de données fait les calculs et communique avec la mémoire. La fenêtre d'instruction sert d'interface entre les deux en accumulant les instructions décodées et prêtes à être exécutée.
Dans les grandes lignes, il suffit de dupliquer les séquenceurs et les registres. Il est obligatoire de dupliquer les registres pour que chaque programme ait son ensemble de registres rien qu'à lui. Cela demande soit un banc de registre par programme, soit un banc de registre commun géré par fenêtrage de registre (chaque programme ayant sa propre fenêtre de registres). Il faut aussi dupliquer le séquenceur en autant de programmes qu'on veut exécuter en simultané, afin que chaque séquenceur s'occupe d'un programme bien à lui. Mais quelques optimisations, dépendantes de l'architecture, permettent d'éviter de dupliquer une bonne partie du séquenceur.
Au minimum, il faut utiliser plusieurs "Program Counter", vu que le processeur charge des instructions en provenance de programmes différents. Un circuit se charge de choisir le "Program Counter" - le thread - qui a la chance de charger ses instructions. Et l’algorithme de choix du programme, l'algorithme d'ordonnancement, dépend du processeur, comme on le verra dans la suite du chapitre. Par exemple, certains processeurs multithreadés changent de programme à chaque cycle d'horloge et donnent la main à chaque programme l'un après l'autre, en boucle. Une autre possibilité est de donner la priorité aux threads qui accèdent le moins à la mémoire RAM (peu de cache miss), à ceux qui ont exécutés le moins d'instructions de branchement récemment, etc. Pour ce qui est de l'unité de chargement et des décodeurs, ils sont en général dupliqués, mais ce n'est pas systématique et tout dépend de l'algorithme d'ordonnancement utilisé. Par exemple, dans le cas où le processeur change de programme à chaque cycle d'horloge, il n'y a besoin que d'une seule unité de chargement et un seul décodeur.
Le partage des ressources matérielles.
Généralement, la fenêtre d'instruction (instruction windows) est partagée entre les différents programmes. Chaque instruction mise en attente (chaque entrée) stocke le numéro du programme, attribué à l'instruction par l'unité de Fetch.
La fenêtre d'instruction est partitionnée en morceaux réservés à un programme bien précis. Si ces morceaux ont la même taille, on parle de "partitionnement statique". Si les morceaux ont des tailles qui changent lors de l’exécution, on parle de "partitionnement dynamique". Le partitionnement statique a l'avantage d'être simple, facile à implémenter. Par contre, il gère mal les situations où un programme est plus gourmand que l'autre. Idéalement, le programme plus gourmand devrait avoir plus d'entrées allouées dans la fenêtre d'instruction, là où le programme qui a besoin de moins devrait laisser la place. Le partitionnement statique donne des portions égale à chaque programme, ce qui marche mal pour de telles situations. À l'opposé, le partitionnement dynamique permet à chaque programme d'avoir le plus de ressources possibles, ce qui maximise les performances. Si jamais un programme a besoin de plus de place que l'autre, il peut réserver un peu plus de place que son concurrent. Mais la gestion du partitionnement est alors plus compliquée, et demande de partitionner efficacement la fenêtre d'instruction, sans quoi les deux programmes exécutées risquent de se marcher dessus et d'entrer en compétition pour les ressources de la fenêtre d'instruction.
Il en est de même pour la TLB, qui est elle aussi partitionnée entre plusieurs "threads". La plupart du temps, le partitionnement est statique sur les TLB dédiées aux instructions, alors que les TLB pour les données sont partagées dynamiquement. C'est le cas sur les architectures Skylake d'Intel, où les 128 entrées de la TLB d'instruction de niveau 1 ont découpées en deux sections de 64 entrées, une par programme/"thread", les autres TLB étant partitionnées dynamiquement.
Les différents types d'architectures multithreadées.
Il existe plusieurs techniques pour ce faire, qui portent les doux noms de "Coarse Grained Multithreading", "Fine Grained Multithreading", "multithreading" total et "Simultaneous MultiThreading". Voyons les en détail.
Le multithreading sur les processeurs à émission unique.
Le Coarse Grained Multithreading change de programme quand un évènement bien précis a lieu. Le changement de programme peut s’effectuer pour des raisons différentes. En général, on change de programme lorsque certaines instructions sont exécutées : accès à la mémoire, branchements, etc. Le cas le plus fréquent est un accès à la mémoire (lors d'un cache miss). Il faut dire que l'accès à la mémoire est quelque chose de très lent, aussi changer de programme et exécuter des instructions pour recouvrir l'accès à la mémoire est une bonne chose. On peut aussi utiliser une instruction de changement de programme, fournie par le jeu d'instruction du processeur.
Le Fine Grained Multithreading change de programme à chaque cycle d'horloge. L'avantage est que deux instructions successives n'appartiennent pas au même programme. Il n'y a donc pas de dépendances entre instructions successives et les circuits liés à la détection ou la prise en compte de ces dépendances disparaissent. Mais pour cela, on est obligé d'avoir autant de programmes en cours d’exécution qu'il y a d'étages de pipeline. Et quand ce n'est pas le cas, on trouver des solutions pour limiter la casse. Par exemple, certains processeurs lancent plusieurs instructions d'un même programme à la suite, chaque instruction contenant quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. Cette technique s'appelle l'anticipation de dépendances.
Sur d'autres processeurs, plusieurs programmes s’exécutent en même temps et chaque programme utilise l'unité de calcul dès que les autres la laissent libre. On parle alors de multithreading total.
Le multithreading sur les processeurs à émission multiple.
Les techniques vues au-dessus peuvent s'adapter sur les processeurs superscalaires, à savoir qui sont capables de démarrer plusieurs instructions simultanément sur des unités de calcul séparées. Dans les deux cas, les unités de calcul exécutent les instruction d'un même programme en même temps. Ce qui veut dire qu'on a pas de situation où une unité de calcul exécute une instruction du premier programme alors qu'une autre exécute une instruction du second programme. Lors d'un cycle d'horloge, les deux unités de calcul doivent exécuter des opérations d'un même programme.
Mais on peut aussi faire en sorte que plusieurs programmes puissent faire démarrer leurs instructions en même temps. À chaque cycle d'horloge, le processeur peut exécuter des instructions provenant de divers programmes. On obtient la technique du Simultaneous Multi-Threading ou SMT.