Les notes de cours

publicité
INF4170 – Architecture des ordinateurs
Notes de cours
1 Introduction
L’architecture des ordinateurs a évolué depuis les années 50. Nous avons été
témoins, au cours des dernières années, d’augmentations vertigineuses des
vitesses et capacités des ordinateurs. L’introduction de machines à jeux
d’instructions réduits formait la crête de la précédente vague de changements.
Le développement des ordinateurs personnels, construits autour des
microprocesseurs, fut le début de ce qui est maintenant considéré comme une
révolution informatique. Certaines des avancées ont été transparentes pour les
utilisateurs, d’autres servent d’arguments de marketing au point de s’intégrer à la
culture populaire.
Le développement du matériel n’a qu’un but, aller plus vite. Cet aspect, à lui seul,
ne détermine pas la performance d’un ordinateur. Le logiciel qui s’exécute sur un
ordinateur doit, idéalement, tirer partie d’une configuration matérielle dans le but
d’être optimal. Le matériel doit donc, en contrepartie, répondre de manière
souple et efficace, aux caractéristiques sémantiques des programmes, certaines
de ces caractéristiques sont intrinsèques aux langages de programmation et
systèmes d’exploitation. Les outils de programmation, les systèmes d’exploitation
ainsi que la configuration mémoire des ordinateurs ont également évolué
significativement depuis le début de l’informatique.
Le développement concurrent et parallèle, tantôt en tête, tantôt à la traîne, des
technologies de la programmation et du matériel, empêche de traiter
d’architecture des ordinateurs en négligeant l’aspect logiciel. Cette association
est particulièrement vraie au niveau des jeux d’instructions, et de l’organisation
des composantes. Le cours présentera donc une synthèse des solutions
matérielles aux besoins du logiciel. Il s’attardera aux problèmes, aux réponses
apportées, leurs implications, et dans une certaine mesure leur implantation.
Aujourd’hui, l’ordinateur est synonyme de numérisation de l’information et de
manipulation numérique de celle-ci. Il existe pourtant deux types d’ordinateurs,
l’analogique et le numérique. Les ordinateurs analogiques utilisent des
phénomènes physiques (circuits électriques ou hydrauliques, règles, poids) qui
simulent le modèle désiré, la lecture des tensions et pressions, positions et
courants, de ces systèmes permet la résolution des équations. Ces ordinateurs
(certains appelés intégrateurs ou différentiateurs) étaient d’usage courant jusqu’à
l’arrivée de l’ordinateur numérique, plus simple d’utilisation mais possédant
également leurs lots d’inconvénients. L’ordinateur numérique traite une
information encodée, binaire, grâce à une série de bascules. La conversion d’un
système analogique à un système numérique implique une phase
d’échantillonnage où seules certaines valeurs seront conservées. La densité et la
précision des échantillons établissent la qualité des données, elles influencent,
mais sans être l’unique facteur, la qualité des résultats obtenus. Le passage
inverse, du numérique à l’analogique est possible, l’approximation du système
analogique l’est d’autant plus que la précision et la densité des valeurs
numériques sont grandes (théorème de Shannon ou Nyquist). L’évolution de
l’utilisation des ordinateurs, pour les applications de gestion et scientifiques, s’est
accomplie en préservant la précision des données, des entiers pour les
inventaires et les comptes, des nombres à virgule flottante ou fixe pour des
valeurs décimales.
On attribue généralement à John von Neumann l’invention de l’ordinateur
numérique. Cette consécration est maintenant considérée comme surfaite. Il n’en
demeure pas moins un acteur, et témoin privilégié, des balbutiements des
ordinateurs de la première génération (1938-1953). Ces ordinateurs utilisaient
des lampes à vide et des relais électromécaniques. Les premières machines ne
stockaient pas de programmes en mémoire, elles devaient être programmées à
l’aide de séries d’interrupteurs. L’ENIAC (1946), le EDVAC (1950), le
EDSAC(1951) ainsi que la série des Mark I et Mark II effectuaient des calculs bit
à bit et étaient bâtis de milliers de composantes. La vitesse se mesurait en
microsecondes et la chaleur dissipée par chaque composante se mesurait en
watts plutôt qu’en milliwatts comme pour celles d’aujourd’hui. Le premier
ordinateur commercial, le UNIVAC fut introduit en 1951.
L’invention du transistor en 1948 permit une première miniaturisation des
ordinateurs, bien que cette transition ne fut complétée qu’au début des années
60. Les transistors remplacent avantageusement les lampes à vide, et relais
électromécanique, puisqu’ils occupent un espace restreint, consomment et
dissipent peu d’énergie tout en étant plus résilients. Cette seconde génération
(1952-1963) voit également l’émergence de nouveaux outils, l’assembleur
inventé par Wilkes (1951) permit de développer plus aisément les programmes,
le FORTRAN (1956) par IBM, le COBOL (1959) par l’armée américaine ainsi que
Algol en 1960, établirent les fondements de l’informatique et rendirent plus
accessible l’utilisation des ordinateurs. Les mémoires étaient bâties autour de
pièces magnétisées, les tores, dont l’orientation est modifiée par le passage d’un
courant électrique. Ces mémoires, complexes à construire, avaient un temps
d’accès (1 s), un mégaoctet valait 1 million de dollars. Les coûts d’opération des
ordinateurs étant prohibitifs, la rentabilité des systèmes passait par le
développement de techniques qui alliaient le travail du processeur et les
entrées/sorties. L’objectif de diminuer le temps pris par un programme pour
s’exécuter, se réalisa dans le cadre des systèmes d’exploitation, il demanda de
nouveaux supports par le matériel. L’introduction des disques à accès nonséquentiels, en 1963 par IBM, révolutionna à son tour le monde de
l’informatique.
Les apprentissages de la décennie précédente, tant dans le domaine de la
technique que dans le domaine théorique, servirent de bases pour une nouvelle
génération de machines (1962-1975). La foi dans les capacités techniques, se
heurtant aux limites des connaissances, se traduisit par des machines
extrêmement complexes qui devaient résoudre tous les problèmes et se gérer
elles-mêmes. La stabilité appréhendée des architectures de cette génération
rendit possible la notion de familles d’ordinateurs, le principe sous-jacent étant
de fournir une version d’une architecture à l’échelle de la taille des problèmes à
résoudre et de l’organisation acquérant celle-ci.
La miniaturisation des composantes de cette génération se fit en utilisant des
circuits intégrés, inventés à la fin des années 50. Les premiers circuits intégrés
sont nommés circuits à petite échelle d'intégration (SSI small scale integrated).
La densité des composantes augmenta de manière fulgurante au cours des
années 60, en moyenne, elle doublait chaque année. Cette progression diminua
dans les années 70, elle quadrupla aux trois ans. Ces croissances de
performance se firent dans un contexte où le prix des composantes demeurait
constant, il s’en suivi d'une baisse vertigineuse du prix des éléments de base des
machines. La miniaturisation des composantes implique que la distance à
parcourir entre deux points est moindre, d’où une augmentation de vitesse mais
également une diminution de puissance et des besoins en refroidissement. Le
nombre moins élevé de connexions entre les éléments diminue le nombre de
pannes dues aux bris. La technologie des circuits associée à la fin de cette
période est appelée intégration moyenne (MSI medium scale integrated).
La quatrième génération d’ordinateurs (1972-1985) se développa autour d’un
nouveau niveau dans les circuits intégrés, la haute intégration (LSI large scale
integration), possédant un nombre de composantes de l’ordre de 1000 par
élément. La mémoire virtuelle, introduite par IBM au début des années 70, permit
d’augmenter l’espace adressable des programmes et par conséquent, la taille
des programmes. Notez qu’il existe des ordinateurs dont la taille de la mémoire
physique est supérieure à l’espace adressable. Le prix par bit de la mémoire
bâtie autour de circuits intégrés descendit sous celui du bit de la mémoire
magnétique vers 1974, permettant la mise au rancart des mémoires à tores.
L’introduction des microprocesseurs, Intel introduit le 4004 en 1971, et un des
premiers micro-ordinateurs dans la seconde moitié des années 70, ouvrirent la
voie à une révolution dans le monde de l’informatique. Parallèlement à ces
développements, plusieurs super ordinateurs furent introduits et le traitement
parallèle fit un bond. Certaines machines traitant simultanément plusieurs
données, en exécutant la même instruction sur des éléments de calcul différents,
alors que d’autres traitaient des données et des instructions différentes.
La complexité d’exécution des instructions, les faibles fréquences d’utilisation
des instructions ainsi que de certains modes d’adressage, provoqua dans la
seconde moitié des années 80 une suite d’études sur la possibilité de réduire la
complexité des instructions. L’hypothèse étant que des outils de développement
pouvaient être assez puissants pour générer des suites d’instructions simples à
exécuter sur des machines moins complexes. Les avancées de la miniaturisation
à très grande échelle (VLSI very large scale integrated), aidant à améliorer la
vitesse, et l’utilisation de pipelines, qui traitent en parallèle différentes instructions
rendues à des étapes intermédiaires différentes de leur cycle d’exécution,
permettent de diminuer le taux apparent de cycles nécessaires pour exécuter les
programmes, d’où de meilleures performances. Ces machines sont appelées
sont appelées RISC, pour Reduced Instruction Set Computers, par opposition au
CISC, Complex Instruction Set Computer. Le développement de ces machines
constitue la cinquième génération et nous y sommes toujours.
La contrainte ultime des machines est la lumière, cette contrainte existe sous
plusieurs formes, elle limite la vitesse de propagation du courant électrique (95%
de la vitesse de la lumière), elle limite également le procédé de fabrication des
circuits. Les circuits intégrés sont "gravés" à l’aide de la lumière. Ils sont
constitués de couches superposées d’isolant et de conducteur. On enduit un
morceau de silicium d’une couche d’oxyde de métal, puis d’une émulsion
photosensible, on recouvre le tout d’un masque laissant le tracé du circuit désiré
à découvert, on expose alors le masque à la lumière et l’émulsion est dégénérée,
on cuit l’émulsion qui sert à son tour de masque pour un traitement à l’acide qui
grave le métal, l’émulsion est par la suite lavée. La surface gravée est remplie
d'un conducteur.
La seconde couche est appliquée au-dessus de la première et ainsi de suite. On
atteint actuellement les limites de l’utilisation de la lumière parce que la largeur
des gravures approche la longueur d’onde de la lumière. On travaille à diminuer
la largeur mais cela occasionne plusieurs problèmes. Des techniques sont mises
au point pour utiliser des rayons X et des faisceaux d’électrons.
Les caractéristiques recherchées qui assurent, du moins jusqu’à aujourd’hui, le
succès d’un ordinateur sont : la généralité, soit la capacité d’exécuter toute une
gamme d’applications; l’efficacité, soit la capacité de générer une quantité de
résultats dans un certain laps de temps; et la malléabilité, soit la possibilité
d’avoir plusieurs configurations différentes (famille d’ordinateurs). La disponibilité
en qualité et quantité, l’ouverture du système et sa simplicité, les possibilités de
mises à jour et de remplacement de pièces sont autant de facteurs qui affectent
également le succès d’un ordinateur.
2 Ordinateur simple
2.1
Architecture, implantation et réalisation
L'homme utilise différents niveaux d'abstraction pour simplifier et décomposer,
un objet complexe en plusieurs couches. Dans le cas du matériel, on distingue
trois niveaux: l'architecture, l'implantation et la réalisation. L'architecture décrit
l'aspect présenté à l'utilisateur de la machine. Elle comprend l'assembleur, les
tailles et types de données, le nombre de registres et leurs noms, les modes
d'adressage, les contraintes de programmation. L'implantation décrit le choix des
composantes et leur organisation logique. Par exemple, l'utilisation de bus
parallèle ou série, la taille du cache, la position des unités fonctionnelles sur le
chip. La réalisation représente le matériel: filage, portes et circuits.
2.2
Modèle de von Newmann
Le modèle de von Newmann propose de stocker indifféremment les instructions
et les données d’un programme en mémoire de l’ordinateur. La généralité de la
mémoire étant gage de souplesse, chaque case possède sa propre adresse. La
distinction entre instruction et donnée est assurée par un compteur ordinal,
intégré à l’unité de traitement, passant séquentiellement d’une instruction à une
autre. L’unité de traitement contrôle l’accès à la mémoire et aux entrées/sorties.
Le lien unissant la mémoire et l'unité de traitement est un goulot d'étranglement
parce qu'il sert au transfert, tant dans un sens que dans l'autre, des adresses
(vers la mémoire) et des données et instructions (vers l'unité de traitement).
Facteur aggravant, les vitesses de ces composantes sont différentes (1 ordre de
grandeur), contraignant également la performance du système. Une variante de
ce modèle, appelé architecture de Harvard, sépare la mémoire de données des
instructions. Elle est fréquemment retrouvée dans les caches parce que le
comportement des références aux instructions est différent de celui des accès
aux données. Cette variante n'est cependant pas à l'abri de problèmes, que nous
verrons plus loin dans la section sur les mémoires.
2.3
Composantes et cycle des instructions
La section précédente décrit une machine simple. Ses principales composantes
sont la mémoire, les périphériques et l'unité centrale de traitement. Cette
dernière est constituée de l'unité arithmétique et logique (UAL) et de l'unité de
contrôle. Le liens physiques unissant les composantes sont appelés bus. Ils
peuvent être aussi bien utilisés pour contrôler les composantes que transmettre
des données et instructions.
Pour exécuter un programme, l’ordinateur traite une série d’instructions. Dans le
but d’exécuter une instruction, les étapes à suivre sont la localisation de
l’instruction à exécuter, son transfert vers l’unité centrale de traitement, son
décodage, la lecture de ses opérandes, le calcul ou traitement, l’écriture des
résultats et le calcul de la prochaine valeur du compteur ordinal. Le passage
d’une opération à la suivante est provoqué par le battage de mesure réalisé par
l’horloge du système, à chaque pulsation correspond l’exécution d’une partie
d’une opération. C'est l'unité de contrôle qui contient, pour chaque instruction,
toutes les parties à réaliser. Le stockage du contenu de la mémoire entre deux
parties consécutives est assuré par des registres temporaires. Ceux-ci agissent
comme des interfaces entre deux pièces du processeur.
C'est le propre des programmes que de choisir entre plusieurs cas en calculant
et comparant des expressions à l'aide de l'ULA. L'exécution d'une instruction par
l'UAL produit deux résultats. Le premier, arithmétique ou logique à proprement
parler, va être stocké en mémoire par la prochaine étape du cycle de l'instruction.
Le second résultat est transmis au registre d'état associé à l'unité de contrôle.
Celui-ci indique les débordements, retenues, emprunts, signe ou égalité avec
zéro du résultat principal. Il participe donc, dans le cas des branchements, au
calcul de la prochaine valeur du compteur ordinal.
Tel que décrit, le cycle des instructions permet d'exécuter tous les programmes.
Cette image est trompeuse, puisqu'elle implique une boucle sans fin où
l'ordinateur ne peut répondre à un événement extérieur. A cet effet, le cycle des
instructions supporte les interruptions. Cette capacité permet d'assurer un temps
de réponse acceptable et à point pour, par exemple, traiter une division par zéro,
un défaut de page, un caractère tapé au clavier, etc... Suite à l'interruption, le
cycle de l'instruction peut être repris, au début de l'instruction ou, au point où
s'est produite l'interruption. Cette reprise dépend des actions produites par
l'instruction sur l'état de la machine.
2.4
Niveaux matériels et logiciels
En introduction, on a mentionné qu'un ordinateur est composé de matériel et
logiciel. Ce mélange est stratifié, il se décompose en trois niveaux, le niveau
application (les programmes usagers), le niveau système (compilateurs, éditeurs
de liens, système d'exploitation, bibliothèques partagées) et le niveau matériel
(sa couche supérieure faite des séquences d'opérations, contenues par l'unité de
contrôle, et sa couche inférieure, la circuiterie). La souplesse, c'est-à-dire la
capacité d'être modifié diminue du niveau application vers le niveau matériel,
alors que les coûts de développement sont inversés. Par exemple, on estime à
350 personnes/année le développement du Pentium III, le développement d'un
logiciel qui a la même durée de vie peut être de 30 personnes/années. Les coûts
prohibitifs du développement du matériel alliés aux besoins des applications
(compatibilité entre plate-formes, interfaces, réutilisation du code, simplicité
d'installation et standardisation) ont eu pour effet d'imposer au niveau
intermédiaire des contraintes. Un effort particulier est fait pour développer un
niveau système polyvalent et performant. Cet effort est également astreint à
remplir une condition marketing de l'informatique, le développement
d'applications doit être simple et est entrevu par l'industrie comme devant être à
la portée de tous. Ces conditions sont vues comme garantes de la pénétration
dans le quotidien des gens.
2.5
Evolution des modèles d'architecture
Il existe plusieurs architectures, on note toutefois une évolution. Celle-ci est
particulièrement visible lorsque l'on s'attarde aux jeux d'instructions. Le modèle le
plus simple est sans contredits, l'architecture à pile. Cette architecture se prête à
merveille à une évaluation post-fixe des expressions. Cette architecture de base
est encore souvent utilisée comme modèle de référence pour exprimer des
programmes. Les compilateurs extraient de cette représentation simple une
version de code optimisée pour une machine plus complexe. Cette architecture
est encore à la base du traitement des nombres à virgule flottante pour les
architectures Intel. Le processeur n'a besoin que de deux registres internes, le
pointeur de pile et le compteur ordinal pour exécuter des programmes avec cette
architecture (les opérandes étant sur le sommet de pile, le résultat principal de
l'UAL remplace les opérandes).
La seconde architecture est la machine à accumulateur. Elle répond
efficacement aux expressions A := A + B. L'accumulateur étant à la fois
opérande source et destination, il libère un accès mémoire. L'accumulateur est
un registre dédié. Si on attribue une valeur de 100 à un accès mémoire et 10 à
un accès registre, l'architecture à pile requiert une valeur de 300 pour une
opération UAL alors que celle-ci requiert une valeur de 120, soit un gain
appréciable en vitesse. Le i8080, la famille des micro-contrôleurs 6800 et les
6502 sont de bons représentants de ce modèle.
La troisième architecture est une généralisation de l'architecture à accumulateur.
On étend à plusieurs registres la possibilité d'être source et destination, tout en
maintenant le second opérande en mémoire principale ou registre. On parle alors
d'architecture à deux opérandes. Cette évolution n'est pas sans problèmes
puisqu'elle impose l'encodage explicite de tous les opérandes, donc la taille des
instructions augmente. On appelle densité de code la taille nécessaire pour
encoder les instructions, plus le nombre de bits est élevé, avec possiblement des
trous, plus la densité du code est réduite. Les familles Motorola 68000 et Intel
x86 sont des représentants de ce modèle.
En spécifiant différents registres pour les destinations et sources, on introduit un
nouveau modèle, l'architecture à trois opérandes. Cette architecture n'est utilisée
que pour des machines RISC. Une nouvelle vague d'architectures a fait son
apparition. Le nombre de sources et de destinations n'est plus le fait saillant de
celles-ci. Il s'agit plutôt de grouper plusieurs instructions de manière à donner
plus de latitude au processeur pour exécuter en parallèle des instructions dans
des unités super-scalaires. Le parallélisme est géré soit par le compilateur en le
rendant explicite (machines EPIC) ou laissé à la détermination par le matériel
(machines VLIW).
3 Mesures de performance
L'horloge bat la mesure d'une architecture. L'unité de mesure associée à
l'horloge est le Hertz (Hz), 1 Hz représente un cycle par seconde. La période
d'un cycle est mesurée en secondes, elle est l'inverse de la fréquence.
Idéalement, une cadence rapide devrait être garante de l'efficacité des machines.
Cependant, le cycle des instructions étant composé de plusieurs étapes, le
nombre de cycles nécessaire à l'exécution des instructions varie d'une machine
à l'autre.
3.1
Loi de Amdahl
La loi de Amdahl décrit l'impact temporel de l'accroissement de performance d'un
système: t f  (t / F)  (ti  t) où tf représente le temps après modification, t le
temps associé à la partie du procédé qui est accéléré, F le facteur d'accélération,
ti le temps initial. Son application aux architectures se base sur le partage de
certaines étapes du cycle d'exécution ainsi que la répartition des fréquences
d'utilisation des instructions. Cette dernière varie beaucoup d'une classe à
l'autre. Par exemple, les opérations de copie occupent le premier rang avec près
de 60 % des cas. En rendant plus rapides certaines étapes et instructions parmi
les plus fréquentes, la somme des gains devient supérieure à celle de
l'optimisation des cas les moins fréquents. Cette règle est générique et la portée
de son application dépasse le matériel et le logiciel.
3.2
Temps d'exécution
Le temps d'exécution d'un programme est égal au nombre de cycles du
programme multiplié par la période d'un cycle. Cette sommation est exacte et
propose, en première analyse, un métrique qui semble intéressant. Il donne à
l'usager une idée précise pour le programme. Il ne permet pas, cependant, de
comparer deux architectures, ni même deux programmes sur une même
architecture. La comparaison de deux architectures requiert la prise en compte
de toutes les particularités des architectures. Pour couvrir celles-ci, l'équation du
temps d'exécution est modifiée pour utiliser la moyenne du nombre de cycles
par instruction, CPI. On établi donc le temps machine comme étant le produit du
CPI par le nombre d'instructions. Le nombre d'instructions utilisé par un
programme n'est pas significatif puisqu'il est possible de sélectionner différentes
séquences d'instructions pour effectuer le même calcul. Il est par ailleurs
possible d'obtenir une séquence plus courte nécessitant plus de cycles et une
séquence plus longue avec moins de cycles.
3.3
MIPS et MFLOPS
Le MIPS représente un million d'instructions par seconde. Ce métrique en soit
n'est pas significatif puisque les architectures n'utilisent pas les mêmes
instructions. Deux compilateurs sur une architecture donnée ne génèrent pas
des séquences de code identiques d'où des temps et nombres d'instructions
différents. Le MFLOPS représente un million d'opérations à virgule flottante par
seconde. Ce métrique n'est pas significatif étant donné les disparités des jeux
d'instructions pour les nombres à virgule flottante. Certaines architectures
fournissent des opérations complexes sous forme d'instructions alors que
d'autres n'en ont pas ou sont différentes.
3.4
Suites de performance
Pour décrire et comparer des architectures et systèmes différents, des suites de
programmes d'utilisation courante sur ces machines sont utilisées. Puisqu'ils
représentent une utilisation usuelle des machines, ils ont tendance à être
représentatifs des performances. Les plus populaires sont les BYTEMARK du
défunt magazine Byte pour les micro-ordinateurs et les SPECMARK pour les
stations de travail.
Il existe plusieurs suites de validation et performance sur le marché pour évaluer
les compilateurs. Tous les compilateurs commerciaux (et également certaines
versions de gcc) sont optimisés pour avoir de bonnes notes et les programmes
de test sont souvent non représentatifs des conditions réelles d'utilisation. Il n'en
demeure pas moins que les performances enregistrées avec des logiciels et de
opérations d'usage courant sont les meilleurs métriques pour comparer deux
systèmes.
4 Données et instructions
L'unité adressable de base est l'octet formé de 8 bits. Pour représenter des
valeurs, on utilise une suite d'octets, le nombre de valeurs est 2 bits.
Généralement, on appelle mot un groupe d'octets qui supporte les valeurs
entières de base de l'architecture, ce nombre de bits est généralement égal à la
largeur du bus de données de la machine. Les mots ont généralement 2 ou 4
octets. Il est également usuel d'utiliser les termes : double mot et quadruple mot
pour représenter les autres valeurs. La numérotation des octets dans un mot est
à l'origine de plusieurs incompatibilités entre des machines. Le terme consacré
est le "endian". Cette dénomination provient du conte "Le voyage de Gulliver". Au
cours de son périple, il fait naufrage sur une île où les habitants d'une partie sont
en guerre avec les habitants de l'autre. L'origine du conflit est une divergence sur
la méthode à utiliser pour manger un oeuf à la coque. Les premiers commencent
par le bout pointu, les seconds par le bout rond. Par référence au bout pointu on
appelle "little endian" les suites d'octets dont la numérotation attribue la valeur
minimale à l'octet qui se trouve à l'adresse la plus basse alors que, par
opposition, on appelle "big endian" les suites dont le numéro maximal est
associé à l'adresse la plus basse. Pour passer de l'un à l'autre, les octets qui
forment les mots doivent être renversés (l'ordre des bits dans un mot n'est pas
changé).
4.1
Valeurs entières et à virgule flottante
L'encodage des entiers utilise le complément à deux. Cet encodage utilise le bit
le plus significatif pour représenter le signe d'une valeur. Les valeurs négatives
sont calculées en additionnant un au complément logique du nombre. Il est
pratique puisqu'il permet de n'avoir qu'un seul encodage pour la valeur 0 et
l'inspection visuelle indique clairement une valeur négative. Nous verrons plus
tard que l'implantation de la soustraction est très simple en utilisant le
complément à deux.
Les opérations arithmétiques génèrent des débordements si le signe du résultat
est différent du signe des opérandes (seules deux opérandes de même signe
peuvent causer des débordements en addition et deux opérandes de signes
opposés en soustraction). Il y a retenue si l'opération génère un bit
supplémentaire. Notez qu'il peut y avoir débordement sans avoir de retenue et
retenue sans débordement.
FFFF
+ 0001
1 0000
Dans cet exemple, sur 16 bits, la valeur 1 est additionnée à
la valeur -1, le processeur n'a pas "conscience" de la taille
des opérandes, pour lui le patron 0xFFFF pourrait aussi bien
être la partie basse d'une valeur sur 32 bits, il fait donc
l'addition bit à bit et génère un 1 qui se retrouve dans le bit de retenue, la somme
étant nulle, le bit zéro doit être levé et par conséquent le bit négatif ne l'est pas.
Puisque les deux opérandes, traitées sur 16 bits, sont de signes opposés, il n'y a
pas de débordement.
Il existe un autre encodage, le BCD (binary coded decimal) qui encode les
valeurs de 0 à 9 sur les 4 bits inférieurs d'un octet (4 bits sont appelés nibble). Le
nombre prend donc une suite d'octets proportionnelle à la taille des valeurs à
encoder. Cette notation est utile puisqu'elle permet aisément de convertir ses
valeurs en codes de caractères imprimables. On n'a qu'à introduire un préfixe
dans la partie haute des octets. Les architectures supportent des opérations
PACK et UNPACK pour concentrer valeurs.
L'encodage des nombres à virgule flottante se compose de plusieurs parties : un
exposant, une mantisse et un signe. Puisque les nombres décimaux peuvent
être représentés avec des exposants différents, les calculs doivent être faits sur
des valeurs normalisées de manière à réduire les conversions et pertes de
précision. Il existe plusieurs normes qui régissent la manipulation des virgules
flottantes. Plusieurs sont introduites par les manufacturiers, la norme
internationale, depuis 1985, est la IEEE-754. Les normes spécifient les plages de
validité, les formats, les comportements lors des calculs.
On utilise la formule suivante, en IEEE 754, pour représenter les nombres à
virgule flottante: 1signe 1.mantisse 2 (exposant biais) . Un nombre est normalisé
lorsque (en binaire), le 1 est placé directement à gauche de la virgule, la
mantisse consiste donc dans la partie de droite. Puisque le bit le plus significatif
représente le signe, le patron de bits de l'exposant possède également un bit de
signe. Les exposants négatifs représentent des nombres très petits (inférieurs à
1) mais semblent alors supérieurs aux exposants positifs, on introduit un biais
pour faire une translation qui fasse paraître les exposants négatifs plus petits que
les exposants positifs. On nomme "underflow" le débordement inférieur
lorsqu'une valeur est trop petite pour être représentée en format normalisé. Un
débordement supérieur se produit lorsque la valeur est trop grande pour être
représentée en format normalisé. Des patrons de bits particuliers sont associés
aux valeurs zéro (s=0, m=0, e=0), NaN (not a number s), infini (s=0, e=-1, m=0),
- infini (s=1, e=-1, m=0).
4.2
Instructions et adressage
Les instructions contiennent plusieurs champs qui encodent l'opération à
exécuter, la taille des opérandes, le type et la location des opérandes. La
complexité de l'encodage nécessaire pour les instructions implique l'utilisation de
plusieurs mots mémoire. Afin d'améliorer la densité du code, l'encodage est
généralement de taille variable et comporte un mot de base. Les différentes
architectures, telles les machines à pile et accumulateur ont souvent des
opérandes implicites qui permettent de sauver des bits d'encodage.
On nomme modes d'adressage les méthodes d'accès aux opérandes. Le calcul
de l'opérande peut être statique, c'est-à-dire que sa position est fixée lors de la
construction du programme, ou bien dynamique, la position étant calculée lors de
l'exécution du programme. Cette dernière méthode utilise des combinaisons
d'adresses de base et de déplacements fixes ou variables. Ces modes
s'adaptent naturellement aux structures de données élémentaires,
enregistrements et tableaux. Dans le cas de tableaux l'adresse d'un élément est
déterminée par : Base  (i  borneinf ) * taille(élément) . Il existe des modes
d'adressage plus complexes qui permettent de "sauver" des instructions. Par
exemple, pour parcourir un tableau il est souvent souhaitable d'avoir l'opérande
qui pointe vers une source ou destination se déplacer vers le prochain élément,
cette incrémentation, positive ou négative, peut être faite avant ou après l'accès
à la mémoire, on parle respectivement de pré et post incrément (décrément).
Dans le cas des branchements il est souvent utile de pouvoir calculer la
destination par rapport à la position courante du compteur ordinal, on parle alors
d'adresse relative. Il existe une trentaine de modes d'adressage différents qui ont
été implantés, il est cependant rare d'en voir plus d'une dizaine sur une
architecture. La table qui suit dresse la liste des modes de base les plus
courants.
1. Adressage direct: l'instruction contient l'adresse de l'opérande
Op
L
X86
68K
SPARC
PowerPC
Opérande
mov
and.l
call
bl
ax, Data
0xd9cd, d7
routine
routine
où Data est une étiquette
n'existe pas pour l'arithmétique
n'existe pas pour l'arithmétique
2. Adressage registre: l'instruction contient le numéro du registre qui contient
l'opérande
Op
L
X86
68K
SPARC
PowerPC
Reg
inc
add.w
add
and
eax
d6,d7
%i1, %i3, %i1
r31,r31,r30
3. Adressage immédiat: l'opérande est imbriquée dans l'instruction
Op
L
Imm
X86
68K
SPARC
PowerPC
cmp
addq.w
add
andi
ebx, 32
d6, 6
%i1, %i1, 1
r31,r31,0
4. Adressage registre-indirect: l'instruction contient un registre qui contient
l'adresse de l'opérande
Op
L Reg
X86
68K
SPARC
PowerPC
Opérande
call
jmp
ret
blr
[eax]
[A0]
%i7
r12
5. Adressage base-déplacement: l'instruction contient un registre de base et un
déplacement par rapport à cette base.
Op
L Reg
Opérande
Depl
Base
+
X86
68K
SPARC
PowerPC
mov
add.l
ld
lbz
eax,[ebp+8]
d0,8[a6]
%i1,[%g2+32]
r30,12(r3)
6. Adressage base-déplacement-index: l'instruction contient un registre de base,
un registre d'index et possiblement un déplacement.
Op
L Reg
Idx
Opérande
Depl
+
X86
68K
SPARC
PowerPC
mov
move.l
illégal
lbzx
Base
es:[si+$1234], $5678
d0, 8[a1, d0.w]
r3, r4, r5
7. Adressage relatif: l'instruction contient un déplacement qui doit être
additionné au compteur ordinal pour calculer l'étiquette de branchement.
Op
L
X86
68K
SPARC
PowerPC
4.3
Rel
PC = PC + Rel
call
bne
b
b
$+32
$+32
$+32
$+32
Modèles d’exécution
Les programmes sont constitués de code et de data. La disposition des
différentes zones de code et data détermine des modèles d'exécution sur
lesquels reposent les systèmes. Le modèle le plus simple est un ensemble
entrelacé de code et data. Ce modèle est utilisé pour des applications simples. Il
est également utilisé pour les programmes COBOL, langage qui ne supporte pas
les variables locales. Un modèle plus évolué supportant les routines ayant des
variables locales est utilisé par FORTRAN, les variables locales sont réutilisées
par chaque invocation de routine. Le support à la récursivité exige de créer des
contextes qui sont associés à chacune des invocations des routines. La manière
choisie pour implanter ce modèle est une pile. Aujourd'hui il est courant de voir
les programmes divisés en quatre sections, celles-ci s'adaptent à tous les
modèles. La mémoire est divisée en data (constantes, variables initialisées et
non-initialisées), code exécutable, pile (stack) et un espace supportant
l'allocation dynamique (heap).
Pile
Monceau
Code
Data
4.4
Support aux langages
Les langages de programmation utilisent les ressources architecturales pour
implanter leur sémantique. L'inverse peut sembler également vrai, pour des
raisons évidentes, le succès des architectures devrait être lié à l'efficacité de leur
support aux langages de programmation. Ceci n'est cependant pas exact
puisque les architectures sont soumises à plusieurs autres facteurs qui bien
souvent prennent le pas.
L’un des plus importants facteurs influençant la performance est l'ensemble des
conventions d'appel et retour de routines, ces conventions incluent le passage de
paramètres. La sémantique des langages spécifie les ordres d'évaluation et de
passage de paramètres, mais bien souvent la spécification du langage laisse aux
fabricants différents choix d’implantation permettant d’optimiser la génération de
code.
De manière générale le protocole d’entrée comporte les étapes suivantes :
 Passage des paramètres
 Sauvegarde de l'adresse de retour et branchement
 Sauvegarde du contexte de la routine appelante
 Création du contexte de la routine appelée
Le protocole de sortie quand à lui:
 Sauvegarde de la valeur de retour
 Elimination du contexte de la routine appelée
 Rétablissement du contexte de la routine appelante
 Récupération de l'adresse de retour
 Elimination des paramètres et retour
Dans le cas du langage C, on impute complètement la responsabilité aux
procédures appelantes pour le passage et l'élimination des paramètres ainsi que
l’évaluation de gauche à droite des arguments. Les langages du style Pascal ou
Modula permettent à la routine appelée de manipuler les paramètres, ce
passage est plus efficace puisque les routines sont libres de copier ou non les
paramètres non scalaires passés par valeur. Certaines architectures implantent
des instructions spécifiques pour manipuler les contextes (LINK et UNLK,
MOVEM pour le MC680x0 par exemple). D'autres exigent des compilateurs la
création de cadres fixes qui englobent, au temps de compilation, la taille
maximale qui sera occupée par les arguments des appels que la routine fera.
Par exemple, le SPARC utilise une zone de 64 longs directement au dessus du
pointeur de pile pour sauvegarder le contenu d'une fenêtre de registre en cas de
débordement, cet espace doit toujours être libre d'où l'impossibilité de modifier la
taille d'un contexte de façon dynamique. Par contre, dans certains cas comme le
PowerPC, c'est une contrainte du système d'exploitation qui demande de
réserver un espace sur la pile (226 octets, appelé zone rouge).
Paramètres passé par l'appelant
Adresse de retour
FP + déplacement
Ancien pointeur de cadre
Sauvegarde
Variables locales et temporaires
FP
FP - déplacement
SP
Il est usuel pour plusieurs architectures de fournir des registres dédiés aux
pointeurs de pile et de cadre. Le mode d'adressage en base-déplacement est
normalement utilisé, un déplacement positif pour les paramètres et un négatif
pour les variables locales et temporaires.
La programmation orientée-objet utilise généralement des tables de pointeurs
vers des routines pour représenter les méthodes. Les appels requièrent souvent
des recherches dans les hiérarchies de tables pour effectuer un appel, que les
appels soient faits à l'aide d'une routine de recherche spécialisée (dispatcher) ou
un bout de code inséré spécifique à chaque site d'appel (thunk), la sélection de
l'adresse de branchement se fait de manière dynamique, il est donc nécessaire
d'avoir un mode d'adressage indirect pour les instructions d'appel de routine. Ce
même adressage peut également être utilisé pour faire un branchement (sans
retour) dans le cas d'une énoncé switch ou case par exemple.
L'amélioration de la performance d'exécution des branchements passe par
l'utilisation d'un comportement privilégié à la prise ou non de ceux-ci. En fonction
de ces règles, le processeur commence le transfert d'instructions. L'assomption
sous-jacente étant que les boucles sont écrites pour être exécutées plusieurs
fois et que les conditions les plus usitées sont insérées les premières dans les
conditionnelles en cascades. On nomme cette caractéristique : prédiction.
Lorsque les règles sont fixes, on parle de prédiction statique, le compilateur, ou
le programmeur, utilise alors un choix judicieux de négations des expressions
conditionnelles pour maximiser la performance. L'implantation de la prédiction
dynamique impose au processeur de conserver des statistiques de branchement
pris ou non, dans une table de paires <adresse, condition>. Elle est
principalement implantée dans les nouvelles architectures.
4.5
Support aux systèmes d’exploitations
Il existe plusieurs liens entre le système d'exploitation et l'architecture d'un
ordinateur. L'atomicité de certaines instructions qui permettent l'accès exclusif de
la mémoire permet l'exclusion mutuelle. Le support à la mémoire virtuelle impose
l'implantation de mécanismes de pagination et de segmentation. Ces
mécaniques sont transparentes aux utilisateurs mais possèdent des interfaces
système servant à leurs gestions.
4.6
Formats d’exécutables et de fichiers objet
Il existe plusieurs formats de fichiers. Les composantes principales sont des
entêtes décrivant les parties des fichiers, des sections de code et données ainsi
que des entrées utilisées par le chargeur pour terminer l'édition de liens
nécessaire à la mise en marche du programme. Les formats les plus populaires
sont le COFF (Common Object File Format) développé pour UNIX et dont il
existe plusieurs variantes (MS-COFF Microsoft, UCOFF MIPS, SGI et Digital,
XCOFF IBM, PEF Apple), et ELF utilisé sous UNIX et Linux. Ce dernier format
est de plus en plus courant dans les systèmes embarqués. La majorité des
formats permettent d'attribuer les sections (lecture ou lecture/ecriture, adresses
préférées), celles-ci peuvent alors être plus aisément partagées entre les
programmes. Le format binaire le plus simple est l'image pure qui contient tout le
code et les données aux "bonnes" adresses et qui ne requiert pas d'édition lors
du chargement du programme.
Les fichiers exécutables sont généralement accompagnés de fichiers de
symboles servant au déboguage. Ces fichiers contiennent une représentation
des types utilisés par le programmes, une table de routines qui contient les
références vers les fichiers et les paires <compteur ordinal, numéro de ligne>,
les variables locales et globales. Le déboggeur se construit une image du
programme lors du chargement en utilisant ces tables et permet ainsi à l'usager
de travailler avec des symboles plutôt que des adresses et des instructions
machines.
4.7
Bibliothèques partagées et PIC
La mise en oeuvre de l'édition de liens au début de l'exécution ou lorsqu'une
partie du programme est déplacée en mémoire impose une perte de
performance. Cette dégradation de performance peut être compensée en
utilisant des références relatives pour les appels, de cette manière il n'est pas
nécessaire de relocaliser les adresses des routines. On appelle cette méthode
PIC pour Position Independent Code. La généralisation de cette méthode aux
bibliothèques partagées demande alors de créer une table de contenu pour
chaque routine externe. De cette manière, les appels locaux sont relatifs et les
appels externes sont indirects au travers de cette table. Pour les références du
code vers les données, le problème est un peu plus complexe puisque l'on doit
prendre les adresses (pointeurs), la méthode utilisée consiste à représenter une
adresse comme la somme de l'adresse de la routine et du déplacement de la
donnée par rapport à celle-ci, au début des routines on ajoute au prologue des
instructions qui extraient la valeur courante du compteur ordinal, celle-ci est
ensuite additionnée au déplacement. Comme il est impossible de distinguer
entre les données de la bibliothèque et les données d'une autre, le data global
est toujours accédé en double indirection, la somme calculée est un pointeur
vers un pointeur, ces pointeurs font partie de la même table d'indirections que les
routines externes. On appelle souvent cette table GOT pour Global Offset Table.
Ainsi le code peut être partagé par plusieurs programmes. Seules les GOT et le
data sont spécifiques à chaque programme.
Téléchargement