Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading

Vous pensez surement qu'il faut obligatoirement plusieurs cœurs pour exécuter plusieurs programmes en parallèle, mais sachez que c'est faux ! Mais 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 processeurModifier

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 il se peut parfaitement qu'elles ne suffisent pas. Ou alors il se peut que le processeur en question ne dispose pas de ces techniques d'optimisation. Si on n'arrive pas à donner du travail à l'unité de calcul avec un seul programme, pourquoi ne pas remplir les vides avec les instructions d'un autre programme ? C'est la solution des architectures multithreadées : exécuter plusieurs programmes en même temps, de manière à ce que l'unité de calcul soit tout le temps occupée : les temps morts d'un programme sont 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 processeur grand public modernes, qui ont une exécution dans le désordre très performante, ne sont pas des architectures multithréadées.

Les registres sont dupliqués et le séquenceur adaptéModifier

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

 
Aperçu de l'architecture d'un processeur multithreadé dans lequel le séquenceur (ici nommé Fetch) est dupliqué.

La fenêtre d'instructionModifier

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.

 
Instruction buffer et SMT.

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. Dans d'autres cas, 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 : on parle de partitionnement dynamique.

 
Partitionnement de l'Instruction Buffer avec l'hyperthreading.

Les différents types d'architectures multithreadéesModifier

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 uniqueModifier

 
Multithreading et mitigation de la latence mémoire.

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.

 
Coarse Grained Multithreading.

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.

 
Fine Grained Multithreading.

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.

 
Full multithreading.

Le multithreading sur les processeurs à émission multipleModifier

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.

 
CGMT sur processeur superscalaire
 
FGMT sur processeur superscalaire

Mais on peut aussi faire en sorte que plusieurs programmes puissent faire démarrer leurs instructions en même temps. A 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.

 
Simultaneous Multi-Threading