Fonctionnement d'un ordinateur/Le modèle mémoire : alignement et boutisme

Outre le jeu d'instruction et l'architecture interne, les processeurs différent par la façon dont ils lisent et écrivent la mémoire. On pourrait croire qu'il n'y a pas grande différence entre processeurs dans la façon dont ils gèrent la mémoire. Mais ce n'est pas le cas : des différences existent qui peuvent avoir un effet assez important. Dans ce chapitre, on va parler de l'endianess du processeur et de son alignement mémoire : on va s'intéresser à la façon dont le processeur va repartir en mémoire les octets des données qu'il manipule. Ces deux paramètres sont sûrement déjà connus de ceux qui ont une expérience de la programmation assez conséquente. Les autres apprendront ce que c'est dans ce chapitre.

La différence entre mots et bytes

modifier

Avant toute chose, nous allons reparler rapidement de la différence entre un byte et un mot. Les deux termes sont généralement polysémiques, avec plusieurs sens. Aussi, définir ce qu'est un mot est assez compliqué. Voyons les différents sens de ce terme, chacun étant utile dans un contexte particulier.

Dans les chapitres précédents, nous avons parlé des mots mémoire, à savoir des blocs de mémoire dont le nombre de bits correspond à la largeur du bus mémoire. Le premier sens possible est donc la quantité de données que l'on peut transférer entre CPU et RAM en un seul cycle d'horloge. Il s'agit d'une définition basée sur les transferts réels entre processeur et mémoire. Le terme que nous avons utilisé pour cette définition est : mot mémoire. Remarquez la subtile différence entre les termes "mot" et "mot mémoire" : le second terme indique bien qu'il s'agit de quelque de lié à la mémoire, pas le premier. Les deux ne sont pas confondre, et nous allons voir pourquoi.

La définition précédente ne permet pas de définir ce qu'est un byte et un mot, vu que la distinction se fait au niveau du processeur, au niveau du jeu d'instruction. Précisément, elle intervient au niveau des instructions d'accès mémoire, éventuellement de certaines opérations de traitement de données. Dans ce qui va suivre, nous allons faire la différence entre les architectures à mot, à byte, et à chaines de caractères. Voyons dans le détail ces histoires de mots, de bytes, et autres.

Les architectures à adressage par mot

modifier

Au tout début de l'informatique, sur les anciens ordinateurs datant d'avant les années 80, les processeurs géraient qu'une seule taille pour les données. Par exemple, de tels processeurs ne géraient que des données de 8 bits, pas autre chose. Les données en question était des mots. Aux tout début de l'informatique, certaines machines utilisaient des mots de 3, 4, 5, 6 7, 13, 17, 23, 36 ou 48 bits. Pour donner quelques exemples, l'ordinateur ERA 1103 utilisait des mots de 36-bits, tout comme le PDP-10, et ne gérait pas d'autre taille pour les données : c'était 36 bits pour tout le monde.

Les processeurs en question ne disposaient que d'une seule instruction de lecture/écriture, qui lisait/écrivait des mots entiers. On pouvait ainsi lire ou écrire des paquets de 3, 4, 5, 6 7, 13, 17, 23, 36 ou 48 bits. Les registres du processeur avaient généralement la même taille qu'un mot, ce qui fait que les processeurs de l'époque avaient des registres de 4, 8, 12, 24, 26, 28, 31, 36, 48, voire 60 bits.

Les mots en question sont en théorie à distinguer des mots mémoire, mais ce n'est pas souvent le cas en pratique. Les architectures à adressage par mot faisaient en sorte qu'un mot soit de la même taille qu'un mot mémoire. La mémoire était donc découpée en mots, chacun avait sa propre adresse. Par exemple, une mémoire de 64 kilo-mots contenait 65 536 mots, chacun contenant autant de bits qu'un mot. Les mots faisaient tous la même taille, qui variait suivant la mémoire ou le processeur utilisé. Chaque mot avait sa propre adresse, ce qui fait qu'on parlait d'adressage par mot. Il n'y avait qu'une seule unité d'adressage, ce qui fait que le byte et le mot étaient la même chose sur de telles architectures. La distinction entre byte et mot est apparue après, sur des ordinateurs/processeurs différents.

Les architectures à adressage par byte

modifier

Par la suite, des processeurs ont permis d'adresser des données plus petites qu'un mot. Les processeurs en question disposent de plusieurs instructions de lecture/écriture, qui manipulent des blocs de mémoire de taille différente. Par exemple, il peut avoir une instruction de lecture pour lire 8 bits, une autre pour lire 16 bits, une autre 32, etc. Une autre possibilité est celle où le processeur dispose d'une instruction de lecture, qu'on peut configurer suivant qu'on veuille lire/écrire un octet, deux, quatre, huit.

Dans ce cas, on peut faire une distinction entre byte et mot : le byte est la plus petite donnée, le mot est la plus grande. Par exemple, un processeur disposant d'instruction d'accès mémoire capables de lire/écrire 8 ou 16 bits sont dans ce cas. Le byte fait alors 8 bits, le mot en fait 16. La séparation entre byte et mot peut parfois se compléter avec des tailles intermédiaires. Par exemple, prenons un processeur qui dispose d'une instruction de lecture capable de lire soit 8 bits, soit 16 bits, soit 32 bits, soit 64 bits. Dans ce cas, le byte vaut 8 bits, le mot en fait 64, les autres tailles sont des intermédiaires. Pour résumer, un mot est la plus grande unité adressable par le processeur, un byte est la plus petite.

En général, le byte fait 8 bits, un octet. Mais ça n'a pas toujours été le cas, pas mal de jeux d'instructions font exception. L'exemple le plus parlant est celui des processeurs décimaux, qui utilisaient des entiers codés en BCD mais ne géraient pas les entiers codés en binaire normal. De tels processeurs encodaient des nombres sous la forme d'une suite de chiffres décimaux, codés en BCD sur 4 bits. Ils avaient des bytes de 4 bits, voire de 5/6 bits pour les ordinateurs qui ajoutaient un bit de parité/ECC par chiffre décimal. D'autres architectures avaient un byte de 3 à 7 bits.

La taille d'un mot mémoire est de plusieurs bytes : un mot mémoire contient un nombre entier de bytes. La norme actuelle est d'utiliser des bytes d'un octet (8 bits), avec des mots contenant plusieurs octets. Le nombre d'octets dans un mot est généralement une puissance de deux pour simplifier les calculs. Cette règle souffre évidemment d'exceptions, mais l'usage de mots qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage, comme on le verra plus bas.

Sur de telles architectures, il y a une adresse mémoire par byte, et non par mot, ce qui fait qu'on parle d'adressage par byte. Tous les ordinateurs modernes utilisent l'adressage par byte. Concrètement, sur les processeurs modernes, chaque octet de la mémoire a sa propre adresse, peu importe la taille du mot utilisé par le processeur. Par exemple, les anciens processeurs x86 32 bits et les processeurs x86 64 bits utilisent tous le même système d'adressage, où chaque octet a sa propre adresse, la seule différence est que les adresses sont plus nombreuses. Avec un adressage par mot, on aurait eu autant d'adresses qu'avant, mais les mots seraient passés de 32 à 64 bits en passant au 64 bits. Les registres font encore une fois la même taille qu'un mot, bien qu'il existe quelques rares exceptions.

Les architectures à adressage par mot de type hybrides

modifier

Il a existé des architectures adressées par mot qui géraient des bytes, mais sans pour autant leur donner des adresses. Leur idée était que les transferts entre CPU et mémoire se faisaient par mots, mais les instructions de lecture/écriture pouvaient sélectionner un byte dans le mot. Une instruction d'accès mémoire devait alors préciser deux choses : l'adresse du mot à lire/écrire, et la position du byte dans le mot adressé. Par exemple, on pouvait demander à lire le mot à l'adresse 0x5F, et de récupérer uniquement le byte numéro 6.

Il s'agit d'architectures adressables par mot car l'adresse identifie un mot, pas un byte. Les bytes en question n'avaient pas d'adresses en eux-mêmes, il n'y avait pas d'adressage par byte. La sélection des bytes se faisait dans le processeur : le processeur lisait des mots entiers, avant que le hardware du processeur sélectionne automatiquement le byte voulu. D'ailleurs, aucune de ces architectures ne supportait de mode d'adressage base+index ou base+offset pour sélectionner des bytes dans un mot. Elles supportaient de tels modes d'adressage pour un mot, pas pour les bytes. Pour faire la différence, nous parlerons de pseudo-byte dans ce qui suit, pour bien préciser que ce ne sont pas de vrais bytes.

Un exemple est le PDP-6 et le PDP-10, qui avaient des instructions de lecture/écriture de ce type. Elles prenaient trois informations : l'adresse d'un mot, la position du pseudo-byte dans le mot, et enfin la taille d'un pseudo-byte ! L'adressage était donc très flexible, car on pouvait configurer la taille du pseudo-byte. Outre l'instruction de lecture LDB et celle d'écriture DPB, d'autres instructions permettaient de manipuler des pseudo-bytes. L'instruction IBP incrémentait le numéro du pseudo-byte, par exemple.

Les architectures à mot de taille variable

modifier

D'autres architectures codaient leurs nombres en utilisant un nombre variable de bytes ! Dit autrement, elles avaient des mots de taille variable, d'où leur nom d'architectures à mots de taille variable. Il s'agit d'architectures qui codaient les nombres par des chaines de caractères terminées par un byte de terminaison.

La grande majorité étaient des architectures décimales, à savoir des ordinateurs qui utilisaient des nombres encodés en BCD ou dans un encodage similaire. Les nombres étaient codés en décimal, mais chaque chiffre était encodé en binaire sur quelques bits, généralement 4 à 6 bits. Les bytes stockaient chacun un caractère, qui était utilisé pour encoder soit un chiffre décimal, soit un autre symbole comme un byte de terminaison. Un caractère faisait plus de 4 bits, vu qu'il fallait au minimum coder les chiffres BCD et des symboles supplémentaires. La taille d'un caractère était généralement de 5/6 bits.

Un exemple est celui des IBM 1400 series, qui utilisaient des chaines de caractères séparées par deux bytes : un byte de wordmark au début, et un byte de record mark à la fin. Les caractères étaient des chiffres codés en BCD, chaque caractère était codé sur 6 bits. Les calculs se faisaient chiffre par chiffre, au rythme d'un chiffre utilisé comme opérande par cycle d'horloge. Le processeur passait automatiquement d'un chiffre au suivant pour chaque opérande. Chaque caractère/chiffre avait sa propre adresse, ce qui fait l'architecture est techniquement adressable par byte, alors que les mots correspondaient aux nombres de taille variable.

La comparaison entre l'adressage par mot et par byte

modifier

Plus haut, nous avons vu deux types d'adressage : par mot et par byte. Avec la première, ce sont les mots qui ont des adresses. Les bytes n'existent pas forcément sur de telles architectures. Si une gestion des bytes est présente, les instructions de lecture/écriture utilisent des adresses pour les mots, couplé à la position du byte dans le mot. Les lectures/écritures se font pas mots entiers. A l'opposé, sur les architectures adressées par byte, une adresse correspond à un byte et non à un mot.

Les deux techniques font que l'usage des adresses est différent. Entre une adresse par mot et une par byte, le nombre d'adresse n'est pas le même à capacité mémoire égale. Prenons un exemple assez simple, où l'on compare deux processeurs. Les deux ont des mots mémoire de 32 bits, pour simplifier la comparaison. Le premier processeur gère des bytes de 8 bits, et chacun a sa propre adresse, ce qui fait que c'est un adressage par byte qui est utilisé. Le second ne gère pas les bytes mais seulement des mots de 32 bits, ce qui fait que c'est un adressage par mot qui est utilisé.

Dans les deux cas, la mémoire n'est pas organisée de la même manière. Prenons une mémoire de 24 octets pour l'exemple, soit 24/4 = 6 mots de 4 octets. Le premier processeur aura une adresse par byte, soit 24 adresses, et ce sera pareil pour la mémoire, qui utilisera une case mémoire par byte. Le second processeur n'aura que 6 adresses : une par mot. La mémoire a des cases mémoire qui contiennent un mot entier, soit 32 bits, 4 octets.

 
Adressage par mot et par Byte.

L'avantage de l'adressage par mot est que l'on peut adresser plus de mémoire pour un nombre d'adresses égal. Si on a un processeur qui gère des adresses de 16 bits, on peut adresser 2^16 = 65 536 adresses. Avec un mot mémoire de 4 bytes d'un octet chacun, on peut adresser : soit 65 536bytes/octets, soit 65 536mots et donc 65 536*4 octets. L'adressage par mot permet donc d'adresser plus de mémoire avec les mêmes adresses. Une autre manière de voir les choses est qu'une architecture à adressage par byte va utiliser beaucoup plus d'adresses qu'une architecture par mot, à capacité mémoire égale.

L'avantage des architectures à adressage par byte est que l'on peut plus facilement modifier des données de petite taille. Par exemple, imaginons qu'un programmeur manipule du texte, avec des caractères codés sur un octet. S'il veut remplacer les lettres majuscules par des minuscules, il doit changer chaque lettre indépendamment des autres, l'une après l'autre. Avec un adressage par mot, il doit lire un mot entier, modifier chaque octet en utilisant des opérations de masquage, puis écrire le mot final. Avec un adressage par byte, il peut lire chaque byte indépendamment, le modifier sans recourir à des opérations de masquage, puis écrire le résultat. Le tout est plus simple avec l'adressage par byte : pas besoin d'opérations de masquage !

Par contre, les architectures à adressage par byte ont de nombreux défauts. Le fait qu'un mot contienne plusieurs octets/bytes a de nombreuses conséquences, desquelles naissent les contraintes d'alignement, de boutisme et autres. Dans ce qui suit, nous allons étudier les défauts des architectures adressables par byte, et allons laisser de côté les architectures adressables par mot. La raison est que toutes les architectures modernes sont adressables par byte, les seules architectures adressables par mot étant de très vieux ordinateurs aujourd'hui disparus.

Le boutisme : une spécificité de l'adressage par byte

modifier

Le premier problème lié à l'adressage par byte est lié au fait que l'on a plusieurs bytes par mot : dans quel ordre placer les bytes dans un mot ? On peut introduire le tout par une analogie avec les langues humaines : certaines s’écrivent de gauche à droite et d'autres de droite à gauche. Dans un ordinateur, c'est pareil avec les bytes/octets des mots mémoire : on peut les écrire soit de gauche à droite, soit de droite à gauche. Quand on veut parler de cet ordre d'écriture, on parle de boutisme (endianness).

Dans ce qui suit, nous allons partir du principe que le byte fait un octet, mais gardez dans un coin de votre tête que ce n'a pas toujours été le cas. Les explications qui vont suivre restent valide peu importe la taille du byte.

Les différents types de boutisme

modifier

Les deux types de boutisme les plus simples sont le gros-boutisme et le petit-boutisme. Sur les processeurs gros-boutistes, la donnée est stockée des adresses les plus faibles vers les adresses plus grande. Pour rendre cela plus clair, prenons un entier qui prend plusieurs octets et qui est stocké entre deux adresses. L'octet de poids fort de l'entier est stocké dans l'adresse la plus faible, et inversement pour le poids faible qui est stocké dans l'adresse la plus grande. Sur les processeurs petit-boutistes, c'est l'inverse : l'octet de poids faible de notre donnée est stocké dans la case mémoire ayant l'adresse la plus faible. La donnée est donc stockée dans l'ordre inverse pour les octets.

Certains processeurs sont un peu plus souples : ils laissent le choix du boutisme. Sur ces processeurs, on peut configurer le boutisme en modifiant un bit dans un registre du processeur : il faut mettre ce bit à 1 pour du petit-boutiste, et à 0 pour du gros-boutiste, par exemple. Ces processeurs sont dits bi-boutistes.

   

Petit et gros-boutisme ont pour particularité que la taille des mots ne change pas vraiment l'organisation des octets. Peu importe la taille d'un mot, celui-ci se lit toujours de gauche à droite, ou de droite à gauche. Cela n’apparaît pas avec les techniques de boutismes plus compliquées.

 
Comparaison entre big-endian et little-endian, pour des tailles de 16 et 32 bits.
 
Comparaison entre un nombre codé en gros-boutiste pur, et un nombre gros-boutiste dont les octets sont rangés dans un groupe en petit-boutiste. Le nombre en question est 0x 0A 0B 0C 0D, en hexadécimal, le premier mot mémoire étant indiqué en jaune, le second en blanc.

Certains processeurs ont des boutismes plus compliqués, où chaque mot mémoire est découpé en plusieurs groupes d'octets. Il faut alors prendre en compte le boutisme des octets dans le groupe, mais aussi le boutisme des groupes eux-mêmes. On distingue ainsi un boutisme inter-groupe (le boutisme des groupes eux-même) et un boutisme intra-groupe (l'ordre des octets dans chaque groupe), tout deux pouvant être gros-boutiste ou petit-boutiste. Si l'ordre intra-groupe est identique à l'ordre inter-groupe, alors on retrouve du gros- ou petit-boutiste normal. Mais les choses changent si jamais l'ordre inter-groupe et intra-groupe sont différents. Dans ces conditions, on doit préciser un ordre d’inversion des mots mémoire (byte-swap), qui précise si les octets doivent être inversés dans un mot mémoire processeur, en plus de préciser si l'ordre des mots mémoire est petit- ou gros-boutiste.

Avantages, inconvénients et usage

modifier

Le choix entre petit boutisme et gros boutisme est généralement une simple affaire de convention. Il n'y a pas d'avantage vraiment probant pour l'une ou l'autre de ces deux méthodes, juste quelques avantages ou inconvénients mineurs. Dans les faits, il y a autant d'architectures petit- que de gros-boutistes, la plupart des architectures récentes étant bi-boutistes. Précisons que le jeu d'instruction x86 est de type petit-boutiste.

Si on quitte le domaine des jeu d'instruction, les protocoles réseaux et les formats de fichiers imposent un boutisme particulier. Les protocoles réseaux actuels (TCP-IP) sont de type gros-boutiste, ce qui impose de convertir les données réseaux avant de les utiliser sur les PC modernes. Et au passage, si le gros-boutisme est utilisé dans les protocoles réseau, alors que le petit-boutisme est roi sur le x86, c'est pour des raisons pratiques, que nous allons aborder ci-dessous.

Le gros-boutisme est très facile à lire pour les humains. Les nombres en gros-boutistes se lisent de droite à gauche, comme il est d'usage dans les langues indo-européennes, alors que les nombres en petit boutistes se lisent dans l'ordre inverse de lecture. Pour la lecture en hexadécimal, il faut inverser l'ordre des octets, mais il faut garder l'ordre des chiffres dans chaque octet. Par exemple, le nombre 0x015665 (87 653 en décimal) se lit 0x015665 en gros-boutiste, mais 0x655601 en petit-boutiste. Et je ne vous raconte pas ce que cela donne avec un byte-swap...

Cette différence pose problème quand on doit lire des fichiers, du code machine ou des paquets réseau, avec un éditeur hexadécimal. Alors certes, la plupart des professionnels lisent directement les données en passant par des outils d'analyse qui se chargent d'afficher les nombres en gros-boutiste, voire en décimal. Un professionnel a à sa disposition du désassembleur pour le code machine, des analyseurs de paquets pour les paquets réseau, des décodeurs de fichiers pour les fichiers, des analyseurs de dump mémoire pour l'analyse de la mémoire, etc. Cependant, le gros-boutisme reste un avantage quand on utilise un éditeur hexadécimal, quel que soit l'usage. En conséquence, le gros-boutiste a été historiquement pas mal utilisé dans les protocoles réseaux et les formats de fichiers. Par contre, cet avantage de lecture a dû faire face à divers désavantages pour les architectures de processeur.

Le petit-boutisme peut avoir des avantages sur les architectures qui gèrent des données de taille intermédiaires entre le byte et le mot. C'est le cas sur le x86, où l'on peut décider de lire des données de 8, 16, 32, ou 64 bits à partir d'une adresse mémoire. Avec le petit-boutisme, on s'assure qu'une lecture charge bien la même valeur, le même nombre. Par exemple, imaginons que je stocke le nombre 0x 14 25 36 48 sur un mot mémoire, en petit-boutiste. En petit-boutiste, une opération de lecture reverra soit les 8 bits de poids faible (0x 48), soit les 16 bits de poids faible (0x 36 48), soit le nombre complet. Ce ne serait pas le cas en gros-boutiste, où les lectures reverraient respectivement 0x 14, 0x 14 25 et 0x 14 25 36 48. Avec le gros-boutisme, de telles opérations de lecture n'ont pas vraiment de sens. En soit, cet avantage est assez limité et n'est utile que pour les compilateurs et les programmeurs en assembleur.

Un autre avantage est un gain de performance pour certaines opérations. Les instructions en question sont les opérations où on doit additionner d'opérandes codées sur plusieurs octets; sur un processeur qui fait les calculs octet par octet. En clair, le processeur dispose d'instructions de calcul qui additionnent des nombres de 16, 32 ou 64 bit, voire plus. Mais à l'intérieur du processeur, les calculs sont faits octets par octets, l'unité de calcul ne pouvant qu'additionner deux nombres de 8 bits à la fois. Dans ce cas, le petit-boutisme garantit que l'addition des octets se fait dans le bon ordre, en commençant par les octets de poids faible pour progresser vers les octets de poids fort. En gros-boutisme, les choses sont beaucoup plus compliquées...

Pour résumer, les avantages et inconvénients de chaque boutisme sont mineurs. Le gain en performance est nul sur les architectures modernes, qui ont des unités de calcul capables de faire des additions multi-octets. L'usage d'opérations de lecture de taille variable est aujourd'hui tombé en désuétude, vu que cela ne sert pas à grand chose et complexifie le jeu d'instruction. Enfin, l'avantage de lecture n'est utile que dans situations tellement rares qu'on peut légitimement questionner son statut d'avantage. En bref, les différentes formes de boutisme se valent.

L'implémentation de l'adressage par byte au niveau de la mémoire RAM/ROM

modifier

Avant de poursuivre, rappelons que la notion de byte est avant tout liée au jeu d'instruction, mais qu'elle ne dit rien du bus mémoire ! Il est parfaitement possible d'utiliser un bus mémoire d'une taille différente de celle du byte ou du mot. La largeur du bus mémoire, la taille d'un mot, et la taille d'un byte, ne sont pas forcément corrélées. Néanmoins, deux cas classiques sont les plus courants.

Les architectures avec une mémoire adressable par byte

modifier

Le premier est celui où le bus mémoire transmet un byte à la fois. En clair, la largeur du bus mémoire est celle du byte. Le moindre accès mémoire se fait byte par byte, donc en plusieurs cycles d'horloge. Par exemple, sur un processeur 64 bits, la lecture d'un mot complet se fera octet par octet, ce qui demandera 8 cycles d'horloge, cycles d'horloge mémoire qui plus est. Ce qui explique le désavantage de cette méthode : la performance est assez mauvaise. La performance dépend de plus de la taille des données lue/écrites. On prend moins de temps à lire une donnée courte qu'une donnée longue.

L'avantage est qu'on peut lire ou écrire un mot, peu importe son adresse. Pour donner un exemple, je peux parfaitement lire une donnée de 16 bits localisée à l'adresse 4, puis lire une autre donnée de 16 bits localisée à l'adresse 5 sans aucun problème. En conséquence, il n'y a pas de contraintes d'alignements et les problèmes que nous allons aborder dans la suite n'existent pas.

 
Chargement d'une donnée sur un processeur sans contraintes d'alignement.

Les architectures avec une mémoire adressable par mot

modifier

Pour éviter d'avoir des performances désastreuses, on utilise une autre solution : le bus mémoire a la largeur nécessaire pour lire un mot entier. Le processeur peut charger un mot mémoire entier dans ses registres, en un seul accès mémoire. Et pour lire des données plus petites qu'un mot mémoire, le processeur charge un mot complet, mais ignore les octets en trop.

 
Exemple du chargement d'un octet dans un registre de trois octets.

Il y a alors confusion entre un mot au sens du jeu d'instruction, et un mot mémoire. Pour rappel, une donnée qui a la même taille que le bus de données est appelée un mot mémoire. Mais dans ce cas, l'adressage de la mémoire et du CPU ne sont pas compatibles : le processeur utilise une adresse par byte, la mémoire une adresse par mot ! Tout se passe comme si la mémoire était découpée en blocs de la taille d'un mot. La capacité de la mémoire reste inchangée, ce qui fait que le nombre d'adresses utilisables diminue : il n'y a plus besoin que d'une adresse par mot mémoire et non par octet. Il faut donc faire une sorte d'interface entre les deux.

 
Chargement d'une donnée sur un processeur avec contraintes d'alignement.

Par convention, l'adresse d'un mot est l'adresse de son octet de poids faible. Les autres octets du mot ne sont pas adressables par la mémoire. Par exemple, si on prend un mot de 8 octets, on est certain qu'une adresse sur 8 disparaîtra. L'adresse du mot est utilisée pour communiquer avec la mémoire, mais cela ne signifie pas que l'adresse des octets est inutile au-delà du calcul de l'adresse du mot. En effet, l'accès à un octet précis demande de déterminer la position de l'octet dans le mot à partir de l'adresse du octet.

Prenons un processeur ayant des mots de 4 octets et répertorions les adresses utilisables. Le premier mot contient les octets d'adresse 0, 1, 2 et 3. L'adresse zéro est l'adresse de l'octet de poids faible et sert donc d'adresse au premier mot, les autres sont inutilisables sur le bus mémoire. Le second mot contient les adresses 4, 5, 6 et 7, l'adresse 4 est l'adresse du mot, les autres sont inutilisables. Et ainsi de suite. Si on fait une liste exhaustive des adresses valides et invalides, on remarque que seules les adresses multiples de 4 sont utilisables. Et ceux qui sont encore plus observateurs remarqueront que 4 est la taille d'un mot.

Dans l'exemple précédent, les adresses utilisables sont multiples de la taille d'un mot. Sachez que cela fonctionne quelle que soit la taille du mot. Si N est la taille d'un mot, alors seules les adresses multiples de N seront utilisables. Avec ce résultat, on peut trouver une procédure qui nous donne l'adresse d'un mot à partir de l'adresse d'un octet. Si un mot contient N bytes, alors l'adresse du mot se calcule en divisant l'adresse du byte par N. La position du byte dans le mot est quant à elle le reste de cette division. Un reste de 0 nous dit que l'octet est le premier du mot, un reste de 1 nous dit qu'il est le second, etc.

 
Adresse d'un mot avec alignement mémoire strict.

Le processeur peut donc adresser la mémoire RAM en traduisant les adresses des octets en adresses de mot. Il lui suffit de faire une division pour cela. Il conserve aussi le reste de la division dans un registre pour sélectionner l'octet une fois la lecture terminée. Un accès mémoire se fait donc comme suit : il reçoit l'adresse à lire, il calcule l'adresse du mot, effectue la lecture, reçoit le mot à lire, et utilise le reste pour sélectionner l'octet final si besoin. La dernière étape est facultative et n'est présente que si on lit une donnée plus petite qu'un mot.

La division est une opération assez complexe, mais il y a moyen de ruser. L'idée est de faire en sorte que N soit une puissance de deux. La division se traduit alors par un vulgaire décalage vers la droite, le calcul du reste pas une simple opération de masquage. C'est la raison pour laquelle les processeurs actuels utilisent des mots de 1, 2, 4, 8 octets. Sans cela, les accès mémoire seraient bien plus lents.

De plus, cela permet d'économiser des fils sur le bus d'adresse. Si la taille d'un mot est égale à  , seules les adresses multiples de   seront utilisables. Or, ces adresses se reconnaissent facilement : leurs n bits de poids faibles valent zéro. On n'a donc pas besoin de câbler les fils correspondant à ces bits de poids faible.

L'alignement mémoire

modifier

Dans la section précédente, nous avons évoqué le cas où un processeur à adressage par byte est couplé à une mémoire adressable par mot. Sur de telles architectures, des problèmes surviennent quand les lectures/écritures se font par mots entiers. Le processeur fournit l'adresse d'un byte, mais lit un mot entier à partir de ce byte. Par exemple, prenons une lecture d'un mot complet : celle-ci précise l'adresse d'un byte. Sur un CPU 64 bits, le processeur lit alors 64 bits d'un coup à partir de l'adresse du byte. Et cela peut poser quelques problèmes, dont la résolution demande de respecter des restrictions sur la place de chaque mot en mémoire, restrictions résumées sous le nom d'alignement mémoire.

L'alignement mémoire des données

modifier

Imaginons le cas particulier suivant : je dispose d'un processeur utilisant des mots de 4 octets. Je dispose aussi d'un programme qui doit manipuler un caractère stocké sur 1 octet, un entier de 4 octets et une donnée de deux octets. Mais un problème se pose : le programme qui manipule ces données a été programmé par quelqu'un qui n'était pas au courant de ces histoire d'alignement, et il a répartit mes données un peu n'importe comment. Supposons que cet entier soit stocké à une adresse non-multiple de 4. Par exemple :

Adresse Octet 4 Octet 3 Octet 2 Octet 1
0x 0000 0000 Caractère Entier Entier Entier
0x 0000 0004 Entier Donnée Donnée
0x 0000 0008

La lecture ou écriture du caractère ne pose pas de problème, vu qu'il ne fait qu'un seul byte. Pour la donnée de 2 octets, c'est la même chose, car elle tient toute entière dans un mot mémoire. La lire demande de lire le mot et de masquer les octets inutiles. Mais pour l'entier, ça ne marche pas car il est à cheval sur deux mots ! On dit que l'entier n'est pas aligné en mémoire. En conséquence, impossible de le charger en une seule fois

La situation est gérée différemment suivant le processeur. Sur certains processeurs, la donnée est chargée en deux fois : c'est légèrement plus lent que la charger en une seule fois, mais ça passe. On dit que le processeur gère des accès mémoire non-alignés. D'autres processeurs ne gérent pas ce genre d'accès mémoire et les traitent comme une erreur, similaire à une division par zéro, et lève une exception matérielle. Si on est chanceux, la routine d'exception charge la donnée en deux fois. Mais sur d'autres processeurs, le programme responsable de cet accès mémoire en dehors des clous se fait sauvagement planter. Par exemple, essayez de manipuler une donnée qui n'est pas "alignée" dans un mot de 16 octets avec une instruction SSE, vous aurez droit à un joli petit crash !

Pour éviter ce genre de choses, les compilateurs utilisés pour des langages de haut niveau préfèrent rajouter des données inutiles (on dit aussi du bourrage) de façon à ce que chaque donnée soit bien alignée sur le bon nombre d'octets. En reprenant notre exemple du dessus, et en notant le bourrage X, on obtiendrait ceci :

Adresse Octet 4 Octet 3 Octet 2 Octet 1
0x 0000 0000 Caractère X X X
0x 0000 0004 Entier Entier Entier Entier
0x 0000 0008 Donnée Donnée X X

Comme vous le voyez, de la mémoire est gâchée inutilement. Et quand on sait que de la mémoire cache est gâchée ainsi, ça peut jouer un peu sur les performances. Il y a cependant des situations dans lesquelles rajouter du bourrage est une bonne chose et permet des gains en performances assez abominables (une sombre histoire de cache dans les architectures multiprocesseurs ou multi-cœurs, mais je n'en dit pas plus).

L'alignement mémoire se gère dans certains langages (comme le C, le C++ ou l'ADA), en gérant l'ordre de déclaration des variables. Essayez toujours de déclarer vos variables de façon à remplir un mot intégralement ou le plus possible. Renseignez-vous sur le bourrage, et essayez de savoir quelle est la taille des données en regardant la norme de vos langages.

L'alignement des instructions en mémoire

modifier

Les instructions ont toute une certaine taille, et elles peuvent être de taille fixe (toutes les instructions font X octets), ou de taille variable (le nombre d'octets dépend de l'instruction). Dans les deux cas, le processeur peut incorporer des contraintes sur l'alignement des instructions, au même titre que les contraintes d'alignement sur les données vues précédemment.

Pour les instructions de taille fixe, les instructions sont placées à des adresses précises. Par exemple, prenons des instructions de 8 octets. La première instruction prend les 8 premiers octets de la mémoire, la seconde prend les 8 octets suivants, etc. En faisant cela, l'adresse d'une instruction est toujours un multiple de 8. Et on peut généraliser pour toute instruction de taille fixe : si elle fait X octets, son adresse est un multiple de X.

Généralement, on prend X une puissance de deux pour simplifier beaucoup de choses. Notamment, cela permet de simplifier le program counter : quelques bits de poids faible deviennent inutiles. Par exemple, si on prend des instructions de 4 octets, les adresses des instructions sont des multiples de 4, donc les deux bits de poids faible de l'adresse sont toujours 00 et ne sont pas intégrés dans le program counter. Le program counter est alors plus court de deux bits. Idem avec des instructions de 8 octets qui font économiser 3 bits, ou avec des instructions de 16 octets qui font économiser 4 bits.

Les instructions de taille variable ne sont généralement pas alignées. Sur certains processeurs, les instructions n'ont pas de contraintes d'alignement du tout. Leur chargement est donc plus compliqué et demande des méthodes précises qui seront vues dans le chapitre sur l'unité de chargement du processeur. Évidemment, le chargement d'instructions non-alignées est donc plus lent. En conséquence, même si le processeur supporte des instructions non-alignées, les compilateurs ont tendance à aligner les instructions comme les données, sur la taille d'un mot mémoire, afin de gagner en performance.

Sur d'autres processeurs, les instructions doivent être alignées. Dans le cas le plus simple, les instructions doivent être alignées sur un mot mémoire, elles doivent respecter les mêmes contraintes d'alignement que les données. Elles peuvent être plus courtes ou plus longues qu'un mot, mais elles doivent commencer à la première adresse d'un mot mémoire. D'autres architectures ont des contraintes d'alignement bizarres. Par exemple, les premiers processeurs x86 16 bits imposaient des instructions alignées sur 16 bits et cette contrainte est restée sur les processeurs 32 bits.

Que ce soit pour des instructions de taille fixe ou variables, les circuits de chargement des instructions et les circuits d'accès mémoire ne sont pas les mêmes, ce qui fait que leurs contraintes d'alignement peuvent être différentes. On peut avoir quatre possibilités : des instructions non-alignées et des données alignées, l'inverse, les deux qui sont alignées, les deux qui ne sont pas alignées. Par exemple, il se peut qu'un processeur accepte des données non-alignées, mais ne gère pas des instructions non-alignées ! Le cas le plus simple, fréquent sur les architectures RISC, est d'avoir des instructions et données alignées de la même manière. Les architectures CISC utilisent souvent des contraintes d'alignement, avec généralement des instructions de taille variables non-alignées, mais des données alignées. Les deux dernières possibilités ne sont presque jamais utilisées.

De plus, sur les processeurs où les deux sont alignés, on peut avoir un alignement différent pour les données et les instructions. Par exemple, pour un processeur qui utilise des instructions de 8 octets, mais des données de 4 octets. Les différences d'alignements posent une contrainte sur l'économie des bits sur le bus d'adresse. Il faut alors regarder ce qui se passe sur l'alignement des données. Par exemple, pour un processeur qui utilise des instructions de 8 octets, mais des données de 4 octets, on ne pourra économiser que deux bits, pour respecter l'alignement des données. Ou encore, sur un processeur avec des instructions alignées sur 8 octets, mais des données non-alignées, on ne pourra rien économiser.

Un cas particulier est celui de l'Intel iAPX 432, dont les instructions étaient non-alignées au niveau des bits ! Leur taille variable faisait que la taille des instructions n'était pas un multiple d'octets. Il était possible d'avoir des instructions larges de 23 bits, d'autres de 41 bits, ou toute autre valeur non-divisible par 8. Un octet pouvait contenir des morceaux de deux instructions, à cheval sur l'octet. Ce comportement fort peu pratique faisait que l'implémentation de l'unité d"e chargement était complexe.