< Fonctionnement d'un ordinateur

Un programmeur (ou un compilateur) qui souhaite programmer en langage machine peut manipuler ces registres intégrés dans le processeur. A ce stade, il faut faire une petite remarque : tous les registres d'un processeur ne sont pas forcément manipulables par le programmeur. Il existe ainsi deux types de registres : les registres architecturaux, manipulables par des instructions, et les registres internes aux processeurs. Ces derniers servent à simplifier la conception du processeur ou mettre en œuvre des optimisations de performance. Dans ce qui suit, nous allons parler uniquement des registres architecturaux.

Le nombre de registres architecturaux varie suivant le processeur. Généralement, les processeurs RISC et les DSP possèdent un grand nombre de registres. Sur les processeurs CISC, c'est l'inverse : il est rare d'avoir un grand nombre de registres architecturaux manipulables par un programme.

L'utilisation des registres

Les registres se distinguent aussi par le type de données que peuvent contenir leurs registres. Certains ont des registres banalisés qui peuvent contenir tout type de données, tandis que d'autres ont des registres spécialisés pour chaque type de données. Les deux solutions ont des avantages et inconvénients différents. Elles sont toutefois complémentaires et non exclusives.

Les registres spécialisés

Certains processeurs disposent de registres spécialisés, qui ont une utilité bien précise. Leur fonction et leur contenu est ainsi fixé une bonne fois pour toutes. Un registre spécialisé est conçu pour stocker soit des nombres entiers, des flottants, des adresses, etc; mais pas autre chose. Dans les grandes lignes, on peut classer ces registres en deux types : les registres de contrôle et les registres de données. Les premiers stockent des informations nécessaires pour le processeur fasse son travail. Sans ces informations, le processeur ne sait pas quelle est l'instruction suivante, ne peut pas gérer la pile, etc. Les registres de données, quant à eux, mémorisent des données sur lesquelles les instructions agissent directement, les opérandes des calculs.

Les registres de contrôle

Pour ce qui est des registres de contrôle, les plus importants sont clairement le Program Counter, ainsi que les registres pour gérer la pile (le Stack Pointer et le Frame Pointer). Nous avons parlé de ces registres dans les chapitres précédents, ce qui fait que nous n'allons pas revenir dessus. Par contre, nous n’avons pas encore vu le registre d'état, le registre de contrôle par excellence. Il n'est pas présent sur toutes les architectures, notamment sur les jeux d'instruction modernes, mais beaucoup d'architectures anciennes en ont un.

Le registre d'état contient plusieurs bits qui ont chacun une utilité particulière. En général, il contient divers bits utilisés lors d'opérations de comparaisons ou de tests qui servent à donner le résultat de celles-ci. Il contient aussi d'autres bits, mais dont l'interprétation dépend du jeu d'instruction. En général, on trouve les bits suivants :

  • le bit d'overflow, qui est mis à 1 lors d'un débordement d'entiers ;
  • le bit null : précise que le résultat d'une instruction est nul (vaut zéro) ;
  • le bit de retenue, utile pour les additions ;
  • le bit de signe, qui permet de dire si le résultat d'une instruction est un nombre négatif ou positif.

Le registre d'état est mis à jour par les instructions de test/comparaison, mais peut aussi l'être par certaines instructions arithmétiques. Certains bits qu'il contient peuvent servir à autre chose : on peut utiliser un bit pour indiquer si le résultat d'une opération arithmétique est égal ou non à zéro, un autre pour indiquer si ce résultat est négatif ou non, encore un autre pour savoir si le résultat est pair ou impair, etc. Cela permet à des instructions arithmétiques de remplacer des instructions de test. Par exemple, on peut tester si deux nombres sont égaux en soustrayant leur contenu : le bit NULL indiquera le résultat de la comparaison. Ce bit sera à 1 si les deux nombres contiennent la même valeur et à 0 sinon. D'ailleurs, sur certains processeurs, l'instruction de comparaison cmp n'est qu'une soustraction déguisée dans un opcode différent (il faut aussi préciser que le résultat de la soustraction n'est pas sauvegardé dans un registre ou en mémoire et est simplement perdu). C'est le cas sur certains processeurs ARM ou sur les processeurs x86.

Les registres de données

Pour ce qui est des registres de données les plus courants, en voici la liste.

Les registres entiers sont spécialement conçus pour stocker des nombres entiers.

Les registres flottants sont spécialement conçus pour stocker des nombres flottants. L’intérêt de placer les nombres flottants à part des nombres entiers, dans des registres différents peut se justifier par une remarque très simple : on ne calcule pas de la même façon avec des nombres flottants et avec des nombres entiers. La façon de gérer les nombres flottants par nos instructions étant différente de celle des entiers, certains processeurs placent les nombres flottants à part, dans des registres séparés.

Les registres de constante contiennent des constantes assez souvent utilisées. Par exemple, certains processeurs possèdent des registres initialisés à zéro pour accélérer la comparaison avec zéro ou l'initialisation d'une variable à zéro. On peut aussi citer certains registres flottants qui stockent des nombres comme \pi, ou e pour faciliter l'implémentation des calculs trigonométriques).

Les registres d'Index servent à calculer des adresses, afin de manipuler rapidement des données complexes comme les tableaux. Ces registres d'Index étaient utilisés pour effectuer des manipulations arithmétiques sur des adresses. Sans eux, accéder à des données placées à des adresses mémoires consécutives nécessitait souvent d'utiliser du code automodifiant : le programme devait être conçu pour se modifier lui-même en partie, ce qui n'était pas forcément idéal pour le programmeur.

Les registres généraux

Fournir des registres très spécialisés n'est pas très flexible. Prenons un exemple : j'ai un processeur disposant d'un Program Counter, de 4 registres entiers, de 4 registres d'Index pour calculer des adresses, et de 4 registres flottants. Si jamais j’exécute un morceau de programme qui manipule beaucoup de nombres entiers, mais qui ne manipule pas d'adresses ou de nombre flottants, j'utiliserais juste les 4 registres entiers. Une partie des registres du processeur sera inutilisé : tous les registres flottants et d'Index. Le problème vient juste du fait que ces registres ont une fonction bien fixée.

En réfléchissant, un registre est un registre, et il ne fait que stocker une suite de bits. Il peut tout stocker : adresses, flottants, entiers, etc. Pour plus de flexibilité, certains processeurs ne fournissent pas de registres spécialisés comme des registres entiers ou flottants, mais fournissent à la place des registres généraux utilisables pour tout et n'importe quoi. Ce sont des registres qui n'ont pas d'utilité particulière et qui peuvent stocker toute sorte d’information codée en binaire. Pour reprendre notre exemple du dessus, un processeur avec des registres généraux fournira un Program Counter et 12 registres généraux, qu'on peut utiliser sans vraiment de restrictions. On pourra s'en servir pour stocker 12 entiers, 10 entiers et 2 flottants, 7 adresses et 5 entiers, etc. Ce qui sera plus flexible et permettra de mieux utiliser les registres.

Les méthodes hybrides

Dans la réalité, nos processeurs utilisent souvent une espèce de mélange entre les deux solutions. Généralement, une bonne partie des registres du processeur sont des registres généraux, à part quelques registres spécialisés, accessibles seulement à travers quelques instructions bien choisies. Certains processeurs sont très laxistes : tous les registres sont des registres généraux, même le Program Counter. Sur ces processeurs, on peut parfaitement lire ou écrire dans le Program Counter sans trop de problèmes. Ainsi, au lieu d'effectuer des branchements sur notre Program Counter, on peut simplement utiliser une instruction qui ira écrire l'adresse à laquelle brancher dans notre registre. On peut même faire des calculs sur le contenu du Program Counter : cela n'a pas toujours de sens, mais cela permet parfois d'implémenter facilement certains types de branchements avec des instructions arithmétiques usuelles.

L'adressage des registres architecturaux

Outre leur taille, les registres du processeur se distinguent aussi par la manière dont on peut les adresser, les sélectionner. Les registres du processeur peuvent être adressés par trois méthodes différentes. A chaque méthode correspond un mode d'adressage différent. Les modes d'adressage des registres sont les modes d'adressages absolu (par adresse), inhérent (à nom de registre) et/ou implicite.

Les registres nommés

Dans le premier cas, chaque registre se voit attribuer une référence, une sorte d'identifiant qui permettra de le sélectionner parmi tous les autres. C'est un peu la même chose que pour la mémoire RAM : chaque byte de la mémoire RAM se voit attribuer une adresse bien précise. Pour les registres, c'est un peu la même chose : ils se voient attribuer quelque chose d'équivalent à une adresse, une sorte d'identifiant qui permettra de sélectionner un registre pour y accéder. Cet identifiant est ce qu'on appelle un nom de registre. Ce nom n'est rien d'autre qu'une suite de bits attribuée à chaque registre, chaque registre se voyant attribuer une suite de bits différente. Celle-ci sera intégrée à toutes les instructions devant manipuler ce registre, afin de sélectionner celui-ci. Ce numéro, ou nom de registre, permet d'identifier le registre que l'on veut, mais ne sort jamais du processeur : ce nom de registre, ce numéro, ne se retrouve jamais sur le bus d'adresse. Les registres ne sont donc pas identifiés par une adresse mémoire.

Adressage des registres via des noms de registre.

Les registres adressés

Mais il existe une autre solution, assez peu utilisée. Sur certains processeurs assez rares, on peut adresser les registres via une adresse mémoire. Il est vrai que c'est assez rare, et qu'à part quelques vielles architectures ou quelques microcontrôleurs, je n'ai pas d'exemples à donner. Mais c'est tout à fait possible ! C'est le cas du PDP-10.

Adressage des registres via des adresses mémoires.

Les registres adressés implicitement

Les registres de contrôle n'ont pas forcément besoin d'avoir un nom. Par exemple, la gestion de la pile se fait alors via des instructions Push et Pop qui sont les seules à pouvoir manipuler ces registres. Toute manipulation des registres de pile se faisant grâce à ces instructions, on n'a pas besoin de leur fournir un identifiant pour pouvoir les sélectionner. C'est aussi le cas du registre d'adresse d'instruction : sur certains processeurs, il est manipulé automatiquement par le processeur et par les instructions de branchement. C'est aussi le cas pour le Program Counter : à part sur certains processeurs vraiment très rares, on ne peut modifier son contenu qu'en utilisant des instructions de branchements. Idem pour le registre d'état, manipulé obligatoirement par les instructions de comparaisons et de test, et certaines opérations arithmétiques.

Dans ces cas bien précis, on n'a pas besoin de préciser le ou les registres à manipuler : le processeur sait déjà quels registres manipuler et comment, de façon implicite. Le seul moyen de manipuler ces registres est de passer par une instruction appropriée, qui fera ce qu'il faut. Les registres adressés implicitement sont presque toujours des registres de contrôle, beaucoup plus rarement des registres de données. Mais précisons encore une fois que sur certains processeurs, le registre d'état et/ou le Program Counter sont adressables, pareil pour les registres de pile. Inversement, il arrive que certains registres de données puissent être adressés implicitement, notamment certains registres impliqués dans la gestion des adresses mémoire.

La taille des registres architecturaux

Vous avez certainement déjà entendu parler de processeurs 32 ou 64 bits. Derrière cette appellation qu'on retrouve souvent dans la presse ou comme argument commercial se cache un concept simple. Il s'agit de la quantité de bits qui peuvent être stockés dans les registres principaux. Les registres principaux en question dépendent de l'architecture. Sur les architectures avec des registres généraux, la taille des registres est celle des registres généraux. Sur les autres architectures, la taille mentionnée est généralement celle des nombres entiers mais les autres registres peuvent avoir une taille totalement différente. Sur les processeurs x86, un registre pour les nombres entiers contient environ 64 bits tandis qu'un registre pour nombres flottants contient entre 80 et 256 bits (suivant les registres utilisés).

Le nombre de bits que peut contenir un registre est parfois différent de la largeur du bus de données (c'est à dire du nombre de bits qui peuvent transiter en même temps sur le bus de données). Exemple : sur les processeurs x86-32 bits, un registre stockant un entier fait 32bits alors que le bus de données peut contenir 64 bits en même temps. Cela a une petite incidence sur la façon dont une donnée est transférée entre la mémoire et un registre. On peut donc se retrouver dans deux situations différentes : soit le bus de données a une largeur égale à la taille d'un registre, soit la largeur du bus de données est plus petite que la taille d'un registre. Dans le premier cas, le bus de données peut charger en une seule fois le nombre de bits que peut contenir un registre. Dans le second cas, on ne peut pas charger le contenu d'un registre en une fois, et on doit charger ce contenu morceau par morceau.

Aujourd'hui, les processeurs utilisent presque tous des registres de la même taille que le byte, qui est une puissance de 2 (8, 16, 32, 64, 128, 256, voire 512 bits). Mais cette règle souffre évidemment d'exceptions. Aux tout débuts de l'informatique, certaines machines utilisaient des registres de 3, 7, 13, 17, 23, 36 et 48 bits ; mais elles sont aujourd'hui tombées en désuétude. On peut aussi citer les processeurs dédiés au traitement de signal audio, que l'on trouve dans les chaines HIFI, les décodeurs TNT, les lecteurs DVD, etc. Ceux-ci utilisent des registres de 24 bits, car l'information audio est souvent codée par des nombres de 24 bits.

L'usage de bytes qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage. Les problèmes en question surviennent sur les processeurs qui adressent la mémoire par mot (pour rappel, un mot est un groupe de plusieurs bytes). À partir de l'adresse d'un byte, le processeur doit en déduire l'adresse du mot à lire. Et une fois le mot récupéré, le processeur doit décider quel est le byte à prendre en compte. Un mot contenant N bytes, le calcul de l'adresse du mot est une simple division : on divise l'adresse du byte par N. Le reste de la division correspond à la position du byte dans le mot. Avec , la division est un simple décalage et le modulo est un simple ET logique, deux opérations très rapides. Si ce n'est pas le cas, on doit faire une vraie division et un vrai modulo, deux opérations excessivement lentes.

Les optimisations liées aux registres architecturaux

Afin d'améliorer les performances, les concepteurs des processeurs ont optimisé les registres architecturaux au mieux. Leur nombre, leur taille, leur adressage : tout est optimisé au maximum sur les jeux d'instruction dignes de ce nom. Mais sur certains processeurs, on trouve des optimisations assez spéciales, qui visent à dupliquer les registres architecturaux sans que cela se voit dans le jeu d'instruction. Pour le dire autrement, un registre architectural correspond à plusieurs registres physiques dans le processeur. Sur les processeurs récents, on utilise des techniques de renommage de registre, que l'on ne peut pas aborder pour le moment (par manque de concepts importants). Mais outre le renommage de registres, d'autres techniques similaires sur le principe sont possibles, comme le fenêtrage de registres.

Le fenêtrage de registres

Fenêtre de registres.

Plus un processeur a de registres architecturaux, plus leur sauvegarde prend du temps. Pour limiter le temps de sauvegarde des registres, certains processeurs utilisent le fenêtrage de registres, une technique qui permet d'intégrer cette pile de registre directement dans les registres du processeur. Cette technique duplique chaque registre architectural en plusieurs exemplaires qui portent le même nom. Chaque ensemble de registre architecturaux forme une fenêtre de registre, qui contient autant de registres qu'il y a de registres architecturaux. Lorsqu'une fonction s’exécute, elle se réserve une fenêtre inutilisée, et peut utiliser les registres de la fenêtre comme bon lui semble : une fonction manipule le registre architectural de la fenêtre réservée, mais pas les registres avec le même nom dans les autres fenêtres. Ainsi, pas besoin de sauvegarder les registres de cette fenêtre, vu qu'ils étaient vides de toute donnée. S'il ne reste pas de fenêtre inutilisée, on est obligé de sauvegarder les registres d'une fenêtre dans la pile.

Cet article est issu de Wikibooks. Le texte est sous licence Creative Commons - Attribution - Partage dans les Mêmes. Des conditions supplémentaires peuvent s'appliquer aux fichiers multimédias.