Professional Documents
Culture Documents
Architecture Des Ordinateurs - Théorie - 2324
Architecture Des Ordinateurs - Théorie - 2324
DES ORDINATEURS
THÉORIE
François SCHUMACKER, Ir
http://creativecommons.org/licenses/by-nc-nd/2.0/be/
La Haute École Libre Mosane (HELMo) attache une grande importance au respect des
droits d’auteur. C’est la raison pour laquelle nous invitons les auteurs dont une œuvre
aurait été, malgré tous nos efforts, reproduite sans autorisation suffisante, à contacter
immédiatement le service juridique de la Haute École afin de pouvoir régulariser la
situation au mieux.
Architecture des ordinateurs - Théorie Introduction
Introduction
Objectif
L’objectif premier du cours d’architecture des ordinateurs est de présenter l'aspect matériel de
l'informatique : les architectures d’ordinateurs existantes, le fonctionnement des composants les
plus courants, les méthodes de représentation et de codage de l’information.
11 Histoire de l’informatique
1.1 Des nombres et des hommes
La nécessité de savoir compter est apparue très tôt dans l’histoire de l’humanité. Le comptage servait
initialement à quantifier ses propres richesses : nombre de têtes de bétail, volume des récoltes, etc.
Les échanges commerciaux n’ont fait que renforcer ce besoin de comptage.
Les premiers systèmes de comptage étaient basés sur des techniques très simples. À un nombre
donné (quantité dénombrée) correspondaient un nombre équivalent d’éléments physiques :
encoches dans le bois, cailloux (provient du latin calculus, origine du mot calcul), nœuds dans une
corde, jetons, etc. La quantité dénombrée était donc uniquement fonction du nombre d’éléments
utilisés lors du comptage. Les opérations arithmétiques étaient elles aussi très simples : on ajoutait
ou l'on retirait un certain nombre d’éléments.
L’étape suivante fut l’invention des chiffres. Un chiffre est la représentation symbolique d’un
nombre. Les différents chiffres sont matérialisés soit par des objets (p. ex., jetons d’argile) de formes
différentes, soit par un symbole scriptural (barres sur jeton d’argile, hiéroglyphes, chiffres romains,
chiffres arabes…).
Chiffres romains : I, II, III, IV, V, VI, VII, VIII, IX, X, … (pas de zéro)
Chiffres arabes : 1, 2, 3, 4, 5, 6, 7, 8, 9 (toujours pas de zéro 0, il sera inventé plus tard)
Les systèmes de numération primitifs ne sont pas pratiques lorsqu’il s’agit de représenter de grands
nombres. L’homme invente alors la numération de position qui permet de représenter de très
grands nombres d’une manière compacte. La valeur (poids) accordée à un chiffre dépend désormais
de la position à laquelle ce chiffre apparaît dans l’écriture d’un nombre, ainsi que de la base utilisée
par le système de numération.
Avec la numération de position apparaît la nécessité d’inventer le zéro, c’est-à-dire de disposer d’un
chiffre particulier qui représente une absence de valeur dans une position donnée.
Le zéro permet de distinguer 12, 120, 1200, 102, 1002, … sans ambiguïté.
En revanche, on peut constater qu’en base décimale, il n’y a plus de symbole spécifique pour
représenter le nombre 10. Les dix symboles de la base sont : 0, 1, 2, 3, 4, 5, 6, 7, 8 et 9.
La base utilisée par le système de numération a beaucoup varié en fonction des peuples, des
époques, ou des usages. Le tableau ci-dessous présente un résumé des systèmes de numération les
plus connus.
• 1904 – John Ambrose Fleming (physicien et ingénieur anglais) invente le premier tube à
vide : la diode à vide.
• 1936 – Alan Turing (mathématicien, cryptologue et informaticien anglais) publie un article
dans lequel il traite de la possibilité théorique de résoudre (ou non) des problèmes
mathématiques. Il présente une machine universelle, appelée machine de Turing, susceptible
de résoudre tout problème calculable. Il est ainsi à l’origine de la formalisation des concepts
d’algorithme et de calculabilité.
• 1938 – Claude Shannon (ingénieur et mathématicien américain) est le père fondateur de la
théorie de l’information dans laquelle il effectue la synthèse entre les nombres binaires,
l’algèbre de Boole et l’électronique.
• 1936 à 1941 – Konrad Zuse (ingénieur allemand) construit les premiers calculateurs
électromécaniques (Z1 et Z2) basés sur le système binaire. En 1941, le Z3 est un calculateur
universel composé de 2600 relais. Il réalise une multiplication en 5 secondes.
• 1944 à 1947 – Howard Aiken (Harvard) conçoit le Mark I. Énorme calculateur
électromécanique. 72 registres de 23 chiffres décimaux. Multiplication en 6 secondes.
Addition et soustraction en 0,3 seconde.
Son successeur, le Mark II, utilise des relais électromagnétiques rapides, ce qui lui permet de
gagner un facteur de vitesse de 2,6x pour les additions et de 8x pour les multiplications. Il
intègre également des fonctions mathématiques telles que l'inverse, la racine carrée, le
logarithme, l'exponentielle et certaines fonctions trigonométriques en version matérielle.
L’histoire raconte que, le 9 septembre 1947, un papillon de nuit se serait bloqué dans un
relais du calculateur, provoquant ainsi le premier bug informatique !
Les calculateurs Mark sont obsolètes dès leur construction, car l’ère de l’électronique
commence…
• 1945 – John von Neumann (avec Eckert et Mauchley) propose une évolution de l’ENIAC,
appelée EDVAC (Electronic Discrete Variable Automatic Computer). Toutefois, il est
indispensable de résoudre le problème majeur de l’ENIAC : sa programmation très
laborieuse.
John von Neumann a une idée géniale pour faciliter la programmation d’un ordinateur : il
propose une architecture basée sur un système de commande universel. Celui-ci est
contrôlé par un programme dont les instructions sont codées en binaire et stockées en
mémoire, au même titre que les données. L’ordinateur moderne est né !
Mémoire
Unité
Unité de arithmétique Entrée
contrôle et logique
Accumulateur
Sortie
L’architecture (ou machine) de von Neumann constitue une véritable révolution dans la
manière de concevoir un ordinateur, car elle propose une architecture dans laquelle le
programme est enregistré en mémoire (« stored-program » concept), ce qui la rend très
facile à programmer. L’unité de contrôle détermine la logique d’exécution d’un programme.
Le programme est quant à lui défini par une séquence d’instructions stockées en mémoire
sous la forme d’un code binaire. Il est donc aisément modifiable.
La plupart des ordinateurs actuels sont toujours basés sur cette architecture !
Cette architecture n’est pas parfaite. Avec elle apparaît notamment le goulot
d’étranglement de von Neumann : la vitesse de transfert des instructions et des données
entre le processeur et la mémoire est limitée, ce qui réduit sérieusement la vitesse de
traitement effective du processeur. Le problème s’aggrave d’autant plus que la vitesse des
processeurs et la taille des mémoires augmentent. Différentes solutions techniques sont
mises en œuvre pour limiter l’impact du goulot d’étranglement de von Neumann sur les
performances. La mémoire cache est l’une d’entre elles, nous en parlerons au chapitre 6.
• 1949 – Maurice Wilkes construit l'EDSAC (Electronic Delay Storage Automatic Calculator).
Premier ordinateur basé sur l’architecture de Von Neumann. 5.000 opérations mathématiques,
dont 4.000 multiplications par minute.
• 1951 - Eckert et Mauchley construisent l'UNIVAC (UNIVersal Automatic Computer). 5.200 tubes à
vide, 13 tonnes, 1.905 opérations par seconde. Données stockées sur une bande magnétique.
• 1953 – IBM lance l’IBM 701 : 16.000 additions ou 2.200 multiplications par seconde. Viendront
par la suite des versions améliorées (IBM 704 et IBM 709), ainsi que l’IBM 650.
• 1948 – Invention du transistor par John Bardeen, Walter Brattain et William Shockley
(laboratoires Bell).
o Les transistors remplacent les tubes : accroissement de la vitesse et de la fiabilité.
Diminution des coûts de fabrication.
o Essor de l’informatique et naissance des grands acteurs du secteur : DEC, HP, Data
General, …
• 1960 – DEC PDP-1 ($120.000) : le premier « mini-ordinateur ». Mémoire de 4.096 mots de 18
bits. 200.000 instructions par seconde. Premier écran graphique (512x512) et premier jeu vidéo !
• 1962 – IBM 7094 : domine le monde du calcul scientifique au début des années 60.
• 1965 – DEC PDP-8 ($18.000) : le premier ordinateur de masse avec 50.000 exemplaires vendus. Il
introduit le concept de bus pour interconnecter les différents éléments de l’ordinateur.
• 1964 – CDC 6600 (conçu par Seymour Cray): premier superordinateur scientifique. Il introduit la
notion de parallélisme (plusieurs unités fonctionnelles travaillent en même temps) et de
coprocesseurs pour s’occuper de la gestion des tâches et des entrées/sorties.
• 1958 – Premier circuit intégré (Texas Instrument) = nombreux composants dans un seul boîtier.
• 1964 – IBM System/360 : première gamme d’ordinateurs (modèles 30/40/50/65) compatibles
entre eux, mais proposant des puissances croissantes selon les besoins des utilisateurs.
Multiprogrammation (plusieurs programmes en mémoire). Émulation des modèles précédents
(1401 et 7094) par la technique de la microprogrammation.
• 1969 – Système d’exploitation MULTICS (Bell), un ancêtre de UNIX.
La miniaturisation permet une forte augmentation de la puissance tout en diminuant les coûts et
l’encombrement. Cela permet l’éclosion de la micro-informatique et l’apparition de l’ordinateur
personnel (PC).
Périphériques de grande capacité, imprimantes laser de haute qualité, écrans graphiques en couleur.
• 1971 – Premier microprocesseur : 4004 d’Intel (4 bits, 740 KHz, 2.300 transistors, 90.000
opérations par seconde). Une puissance similaire à celle de l’ENIAC : 0,092 MIPS.
• 1972 – Microprocesseur 8008 d’Intel (8 bits, 200 KHz, 3.500 transistors).
• 1973 – Langage C pour le développement d’UNIX.
• 1974 – Premier microprocesseur Motorola : 6800 (8 bits) [TRS-80, Apple II, …].
• 1978 – Microprocesseur 8086 d’Intel (16 bits, 4,77 MHz, 0,33 MIPS). Famille x86.
• 1981 – IBM Personal Computer : design public et donc copié ! (Intel 8088).
• 1984 – Apple Macintosh (Motorola 68000, processeur 16/32bits). Première interface graphique
grand public. Puissance : 1,4 MIPS.
• À partir de 1986 – Processeurs Intel (32 bits) : 80386, 80486, Pentium, Pentium Pro, Pentium II,
Core.
• À partir de 2000 – Processeurs Intel (64 bits) : Pentium 4, Pentium D, Core 2, Core i3, Core i5.
• 2011 – Processeur Intel Core i7 2600K : 64 bits, 1,4 milliard de transistors, 3,4 GHz, 4 cœurs, 8
threads, 128.300 MIPS, gravure 32 nm.
• 2014 – iPhone 6 : 64 bits, 2 milliards de transistors, 1,4 GHz, 25.000 MIPS.
• 2014 – Processeur Intel Core i7 5960X : 64 bits, 2,6 milliards de transistors, 3,5 GHz, 8 cœurs, 16
threads, 238.300 MIPS, gravure 22 nm.
• 2016 – Processeur Intel Xeon Broadwell E5 : 64 bits, 7,2 milliards de transistors, 2,2 GHz, 22
cœurs, 44 threads, gravure 14 nm.
• 2016 – GPU Nvidia GP100 Pascal : 15,3 milliards de transistors, gravure 16 nm, >9.500 GFLOPS !!!
• 2020 – iPhone 11 Pro : 64 bits, 6 cœurs, 8.5 milliards de transistors, gravure 7nm, ≃155 GFLOPS
• 2020 – AMD 3990X : 64 cœurs, 128 threads, 2.9-4.3 GHz, 40 milliards de transistors, >3.700
GFLOPS
En 1965, Gordon Moore (cofondateur d’Intel) constate que depuis 1959 la complexité des circuits
intégrés à base de transistors double tous les ans à coût constant. Il postule que cette croissance
exponentielle va se poursuivre. Ce postulat fut rapidement nommé « (première) loi de Moore ».
En 1975, Moore réévalue sa prédiction et estime que le nombre de transistors sur une puce de
silicium (microprocesseurs, mémoires) doublera tous les deux ans. Cette extrapolation empirique
porte le nom de « deuxième loi de Moore ». Elle s’est révélée très proche de la réalité, puisqu’entre
1971 (date du premier microprocesseur Intel 4004) et 2010 la densité des transistors a effectivement
progressé d’un facteur très proche de 2 tous les deux ans.
Une autre version fréquemment rencontrée, qui est pourtant sans lien avec les énoncés de Moore,
est que d’autres caractéristiques (puissance, capacité, fréquence d’horloge…) doublent tous les 18
mois. Cet énoncé n’est clairement plus vérifié depuis le début des années 2000 en ce qui concerne la
fréquence d’horloge à cause des problèmes de dissipation thermique.
• Il n’est pas possible de réduire indéfiniment la taille d’un transistor. Les transistors composés
de trop peu d’atomes ne sont pas suffisamment fiables.
o Dans un processus de gravure en 22 nm (nanomètre = 10-9 m), un transistor est
composé de seulement 42 atomes.
• Les limites atomiques seront bientôt atteintes :
o Taille d’un atome de silicium (Si) : rayon de van der Waals = 210 pm = 0,21 nm
(pm = picomètre = 10-12 m).
o Finesses de gravures actuelles : 10 nm (puces pour mobiles), 7 nm.
o Perspectives : 5 nm vers 2020/2021, 1 transistor = 7 atomes !
o Limite ultime de la technologie actuelle : 3 nm vers 2021/2022.
• Une densité très forte induit des phénomènes parasites de fuite de courant.
• La quantité d’énergie dissipée devient problématique !
o Chaque fois qu’un transistor change d’état, il libère une petite quantité d’énergie.
Plus il y a de transistors par unité de surface et plus il y a d’énergie dissipée.
o L’énergie dissipée est fonction de la fréquence d’horloge (nombre de transitions par
seconde) et du carré de la tension de fonctionnement (E α V2 x F).
▪ La fréquence est plafonnée à 5 GHz.
▪ La tension peut difficilement descendre en dessous de 0,9V sans causer des
problèmes techniques (bruit thermique).
2 Présentation générale
2 d’un ordinateur
Un système informatique est donc composé d’une partie matérielle (unité centrale de traitement,
mémoire, périphériques…) et d’une partie logicielle (système d’exploitation, applications…).
Un exemple bien connu d’ordinateur est l’ordinateur personnel ou PC (Personal Computer) qui
convient pour un usage domestique ou comme poste de travail dans une entreprise. En voici une
illustration :
écran
clavier souris
haut-parleurs
cd/dvd
carte réseau
caméra
scanner
Un tel système est généralement constitué des éléments décrits ci-après. Cette liste n’est pas
exhaustive. La configuration d’un ordinateur personnel peut varier en fonction de l’utilisation
envisagée.
En fait, l’ordinateur est devenu omniprésent. La miniaturisation de plus en plus poussée des
microprocesseurs, l’évolution impressionnante des performances et la diminution des coûts de
fabrication font que l’ordinateur est utilisé dans des domaines très variés et sous des formes
auxquelles on ne pense pas toujours.
Ordinateurs personnels
• C’est l’ordinateur « traditionnel » tel que nous l’avons décrit ci-dessus avec ses variantes
nomades : portable, ultraportable, netbook, tablette.
• Le prix varie de quelques centaines à quelques milliers d’EUROS.
• Il est utilisé par un seul utilisateur à la fois.
Serveurs
• Un serveur est fondamentalement une version « gonflée » d’un ordinateur personnel. Il est
destiné à être connecté au réseau afin de fournir divers services à des utilisateurs distants :
stockage de fichiers, impression, base de données, serveur Web, calcul.
• Un serveur aura généralement plus d’espace disque, plus de mémoire, plus de processeurs et
une connexion réseau à haut débit. Il n’aura pas nécessairement d’écran ou de clavier, sauf
lors de sa configuration. Souvent, il aura une forme spécialement adaptée à son installation
dans un rack.
• Le prix d’un serveur peut varier de quelques milliers d’EUROS à plusieurs centaines de
milliers.
• Un logiciel spécifique est conçu pour répartir la charge de traitement sur les différentes
machines, ce qui permet de traiter simultanément un très grand nombre de requêtes.
Exemples : services Internet (Google, Amazon, …).
• Une autre utilisation d’une ferme de serveur est de faire collaborer un très grand nombre de
serveurs à la résolution d’un problème unique.
Exemples : simulations numériques, modèles météorologiques, …
Superordinateurs, mainframes
• L’âge d’or des superordinateurs (Cray, Connection Machine, …) aux architectures très
spécifiques est révolu. Les nouveaux « superordinateurs » sont désormais construits sur base
de fermes de serveurs standards qui parviennent à délivrer autant de puissance de calcul que
des superordinateurs à architectures spécifiques, et cela à un prix nettement inférieur.
• Reliques du passé, certains mainframes (successeurs de l’IBM 360) sont encore maintenus en
activité de nos jours, par exemple dans le secteur bancaire. Ceci est dû à l’énorme
investissement qui a été consenti pour mettre ces systèmes en place (logiciels, données,
procédures…) et aux coûts et risques qu’une migration complète et rapide engendrerait.
2.1.3.1 Définition
Un « cloud » n’est rien d’autre qu’une ou plusieurs fermes de serveurs qui fournissent des services
aux utilisateurs connectés à travers Internet.
D’une certaine manière, le « cloud » réinvente le concept de mainframe sur base des technologies
actuelles :
• la ferme de serveur remplace le mainframe,
• l’ordinateur personnel ou le smartphone remplace le terminal,
• Internet remplace la liaison téléphonique par modem.
2.1.3.2 Intérêt
Le fait d’offrir des services sous forme de « cloud » permet de réaliser plusieurs objectifs :
• mutualiser les ressources pour fournir à moindre coût un service à un très grand nombre
d’utilisateurs ;
• la connexion via Internet permet de rendre le service accessible de n’importe où (ou
presque) ;
• le stockage des données dans le « cloud » permet d’offrir de nouveaux services, tels que la
synchronisation de plusieurs appareils, le partage de documents, la gestion collaborative de
documents.
Il convient de noter que toutes ces fonctionnalités sont déjà mises en œuvre au sein d’un réseau
privé ou d’une d’entreprise. Le « cloud » élargit seulement l’horizon à Internet.
L’idée est de louer à une entreprise l’infrastructure matérielle dont elle a besoin (connectivité réseau,
serveurs, stockage), celle-ci étant délocalisée dans le cloud.
Le service fourni aux entreprises dans le modèle « PaaS » inclut non seulement l’infrastructure, mais
également le système d’exploitation et les logiciels de base (base de données, serveur Web, outils de
développement, …) qui permettent à l’entreprise de développer et de déployer rapidement ses
propres applications.
Dans ce dernier modèle, le gestionnaire du cloud fournit un service applicatif à ses clients et est
responsable de l’intégralité de la solution (connectivité, matériel, logiciel). Le client final peut
uniquement paramétrer la solution offerte pour l’adapter à ses besoins. Cette approche permet à
une entreprise de déployer des applications ou services ambitieux sur Internet avec une équipe
technique très réduite, sans avoir à se préoccuper de la gestion opérationnelle d’une infrastructure
complexe. Exemples de solutions « SaaS » : site de vente en ligne, gestion de personnel,
vidéoconférence…
Si l’émergence des services « cloud » offre de nouvelles possibilités et une plus grande flexibilité aux
entreprises, la mise en œuvre de ces services pose néanmoins quelques questions importantes :
• Sécurité et confidentialité des données stockées dans le « cloud » ?
• Disponibilité de l’accès à Internet ?
• Disponibilité du service (Service Level Agreement – SLA) ?
• Dépendance forte à l’égard d’un fournisseur : changement de fournisseur ?
Unité de
contrôle
Unité
arithmétique
et logique
Périphériques d’entrée/sortie
Registres
Mémoire
Disque Imprimante
centrale
Bus d’interconnexion
Une évolution majeure par rapport à l’architecture de Von Neumann, qui est apparue pour la
première fois avec le DEC/PDP-8, est l’introduction d’un bus de communication (ensemble de fils
parallèles) utilisé pour interconnecter les différents composants de l’ordinateur.
Le programme à exécuter et ses données sont tout d’abord chargés en mémoire centrale. On peut
ensuite schématiser l’exécution d’un programme par la répétition de la séquence d’opérations
suivante, que l’on appelle un cycle d’exécution :
C’est le rôle de l’unité de contrôle (ou de commande) d’assurer le séquencement des opérations au
sein du CPU. L’unité arithmétique et logique (ALU) réalise quant à elle les opérations arithmétiques et
logiques demandées par le programme.
Les données utilisées par l’ALU peuvent se trouver dans les registres du processeur ou en mémoire
centrale. Certaines architectures de processeurs imposent que toutes les données manipulées par
l’ALU se trouvent dans les registres (à l’exception bien sûr des opérations de transfert entre un
registre et la mémoire centrale).
Le temps nécessaire pour accéder à (ou écrire) une donnée en mémoire centrale est très long par
rapport au temps nécessaire à l’exécution d’une instruction par l’ALU (10 à 50x). C’est pourquoi on
essaiera d’utiliser au maximum les registres du processeur, par exemple pour stocker les résultats
intermédiaires d’un calcul. Pour réduire l’impact négatif des longs temps d’accès à la mémoire
centrale, on utilise aussi une mémoire intermédiaire plus petite, mais beaucoup plus rapide, appelée
mémoire cache (ou antémémoire).
Dans les architectures modernes, un ordinateur contiendra en général au moins deux bus : un bus
système qui fournit au CPU un accès très rapide à la mémoire et un bus d’entrées/sorties servant à
connecter les contrôleurs d’entrées/sorties.
Bus système
Mémoire Mémoire
CPU
cache centrale
Bus d’entrée/sortie
Disque Imprimante
Périphériques d’entrée/sortie
Niveau 1 – Microarchitecture
• La microarchitecture consiste à connecter et à organiser les éléments de base.
• Les registres et l’ALU sont regroupés au sein du CPU. Une unité de contrôle est créée afin
d’organiser le séquencement des opérations dans le CPU. Suivant les machines, l’unité de
contrôle est réalisée au moyen d’une logique câblée ou d’un microprogramme.
• Une partie du système d’exploitation est stockée au sein de l’ordinateur, dans une mémoire
permanente : c’est le micrologiciel (firmware). Le firmware est exécuté de manière
automatique au démarrage de la machine. Il contient le code permettant la gestion des
fonctions de base : démarrage d’autres processus, gestion de la mémoire, opérations de base
pour les entrées/sorties…. Sa première tâche est de charger en mémoire le reste du système
d’exploitation qui est stocké dans une mémoire secondaire (disque dur ou SSD, DVD, clé
USB…).
• Les systèmes d’exploitation « commerciaux » de type Windows, UNIX sont en fait constitués
d’une multitude de programmes qui vont bien au-delà de la simple gestion du système.
Néanmoins, ils contiennent tous au minimum un noyau (kernel) qui fournit les
fonctionnalités de base évoquées ci-devant.
• Un programme écrit en langage d’assemblage ne peut pas être exécuté directement par le
processeur. Le code symbolique doit d’abord être traduit en code machine. Le programme
qui réalise cette traduction s’appelle un assembleur.
• Le langage d’assemblage est essentiellement utilisé par les programmeurs système pour
écrire des programmes pour les niveaux 2 et 3.
• Il existe deux techniques principales pour exécuter un programme écrit dans un langage de
haut niveau : la compilation et l’interprétation.
soit en un programme en langage d’assemblage qui sera à son tour traduit en code
machine par un assembleur. Le langage C est un bon exemple de langage compilé.
• Le langage Java offre une approche hybride entre la compilation et l’interprétation. En effet,
le code source Java est compilé vers un jeu d’instructions intermédiaire appelé « Java Byte
Code » (code machine Java). Ce jeu d’instructions correspond donc à l’architecture de niveau
2 (ISA) d’un processeur qui serait capable d’exécuter du code machine Java. Un certain
nombre de processeurs Java ont été construits, mais ils restent cantonnés à des projets de
recherche. En pratique, le code machine Java est exécuté par une machine virtuelle Java
(Java Virtual Machine – JVM), c’est-à-dire un interpréteur qui simule un processeur Java en
traduisant à la volée le code machine Java vers le code machine du processeur hôte. Un
programme en code machine Java peut donc être exécuté sur n’importe quel processeur, à
condition de disposer de la machine virtuelle Java correspondante.
• Faciliter la vie du programmeur en lui offrant le jeu d’instructions le plus riche possible.
• Garder une architecture matérielle simple et efficace.
Historiquement, la tendance a été d’ajouter de plus en plus d’instructions au jeu d’instructions d’un
processeur. Ainsi, dans la famille des processeurs Intel x86, les générations successives ont apporté :
Ce type d’architecture est appelée architecture CISC (Complex Instruction Set Computer), c’est-à-dire
« ordinateur à jeu d’instructions complexe ». Exemples : processeurs Intel x86, Motorola 680x0.
Les instructions sophistiquées d’un processeur CISC sont très commodes à utiliser, mais :
La complexité croissante de mise en œuvre des processeurs CISC a amené certains concepteurs de
processeurs à changer complètement l’angle d’approche et à proposer une architecture alternative
appelée RISC (Reduced Instruction Set Computer), c’est-à-dire « ordinateur à jeu d’instructions
réduit». Exemples : PowerPC, ARM, MIPS.
Comme son nom l’indique, une architecture RISC propose un jeu d’instructions limité à un petit
nombre d’opérations simples. Ces instructions ont une taille fixe et le nombre de modes d’adressage
est limité. La mise en œuvre des instructions au niveau matériel est grandement facilitée, ce qui
permet une exécution très rapide. L’unité de contrôle sera généralement câblée. Dans une
architecture RISC, le rôle du compilateur est essentiel. Il doit être capable de produire un code
efficace, qui exploite au maximum les caractéristiques de la machine.
Durant de nombreuses années, l’opposition CISC/RISC a fait rage. Actuellement, on observe une
« fusion » des deux approches. Les processeurs Intel des dernières générations sont en effet basés
sur une architecture de type RISC. La compatibilité avec l’ensemble des instructions complexes x86
est assurée grâce à un microprogramme qui traduit les instructions complexes en une série
d’instructions simples. De leur côté, les processeurs RISC gagnent progressivement en complexité.
C’est ainsi que le jeu d’instructions des processeurs ARM contient maintenant des instructions pour
les calculs en virgule flottante ou vectoriels.
3 Le codage des
3 informations
Le système décimal est celui que nous utilisons le plus couramment dans la vie de tous les jours. Il
utilise les chiffres de 0 à 9. Ce système n’est toutefois pas le plus adapté aux traitements par un
ordinateur.
Le choix le plus approprié pour représenter des informations dans un ordinateur c’est le binaire. Le
système binaire utilise uniquement deux symboles (0 et 1), ce qui correspond naturellement au
comportement des composants électroniques. Ceux-ci manipulent également deux valeurs
distinctes : présence ou absence de courant (ou de tension).
Les systèmes octal et hexadécimal permettent une représentation plus commode et surtout plus
compacte des données, tout en offrant une conversion aisée depuis et vers le système binaire
comme nous l’expliquerons à la section 3.2.3.
Le système hexadécimal comportant 16 symboles, il est nécessaire d’ajouter 6 symboles en plus des
chiffres 0 à 9. Il s’agit des lettres A, B, C, D, E et F, qui représentent respectivement les valeurs 10, 11,
12, 13, 14 et 15.
L’hexadécimal est fréquemment utilisé en informatique. Il sert par exemple à exprimer une nuance
de couleur lors du développement de pages Web (#FFA500 = orange) ; à représenter la valeur d’une
empreinte cryptographique MD5 ("test MD5" → 212A4C389DB4ECFACF61817A711CF892), etc.
Le tableau qui suit donne la valeur des nombres décimaux de 0 à 15 en binaire, octal et hexadécimal.
Si nous connaissons la base utilisée par un système de numération, nous pouvons définir de manière
générale un nombre entier N exprimé en base B de la manière suivante :
Exemples
L’indice mentionné juste après un nombre indique la base de numération utilisée (123410 → décimal,
12348 → octal). Si la base n’est pas mentionnée de manière explicite, on considérera généralement
qu’il s’agit de la base 10, sauf si le contexte permet de connaître la base utilisée.
NB = an Bn + an-1 Bn-1 + … + a0 B0 + a-1 B-1 + … + a-m B-m = ∑𝑛𝑖=−𝑚 𝑎𝑖 𝐵𝑖 Forme expansée
NB = anan-1 … a2a1a0 , a-1 … a-m (ex: 1234,56) Forme normale
Exemples
+ 0 1 2 3 4 5 6 7 8 9
0 0 1 2 3 4 5 6 7 8 9
1 1 2 3 4 5 6 7 8 9 10
2 2 3 4 5 6 7 8 9 10 11
3 3 4 5 6 7 8 9 10 11 12
4 4 5 6 7 8 9 10 11 12 13
5 5 6 7 8 9 10 11 12 13 14
6 6 7 8 9 10 11 12 13 14 15
7 7 8 9 10 11 12 13 14 15 16
8 8 9 10 11 12 13 14 15 16 17
9 9 10 11 12 13 14 15 16 17 18
Figure 3-3 - Table d’addition en décimal
Pour calculer la somme de deux nombres d’un chiffre, il suffit de regarder dans la table d’addition la
valeur qui est indiquée dans la case située à l’intersection de la ligne et de la colonne qui
correspondent aux 2 nombres à additionner. Par exemple, on trouve que 6 + 8 = 14.
Pour additionner 2 nombres de tailles plus grandes, on commence par placer les 2 nombres l’un en
dessous de l’autre en alignant les chiffres des positions correspondantes. Ensuite, en procédant de la
droite vers la gauche, on additionne tous les chiffres d’une même colonne, on inscrit le chiffre des
unités du total comme chiffre de résultat et on reporte une retenue de 1 dans la colonne de gauche
suivante si ce total atteint 10 ou plus. Et ainsi de suite…
Pour réaliser une multiplication à la main, la procédure est un peu plus complexe, mais en définitive
les étapes à réaliser consistent également à additionner ou à multiplier des nombres décimaux d’un
seul chiffre. À cette fin, nous avons également dû mémoriser la table de multiplication suivante :
x 0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0
1 0 1 2 3 4 5 6 7 8 9
2 0 2 4 6 8 10 12 14 16 18
3 0 3 6 9 12 15 18 21 24 27
4 0 4 8 12 16 20 24 28 32 36
5 0 5 10 15 20 25 30 35 40 45
6 0 6 12 18 24 30 36 42 48 54
7 0 7 14 21 28 35 42 49 56 63
8 0 8 16 24 32 40 48 56 64 72
9 0 9 18 27 36 45 54 63 72 81
Figure 3-4 - Table de multiplication en décimal
2 3 4 7 6 , 2
x 2 1 3 0 , 4
9 3 9 0 4 8 ← 4 x 234762 [4x2=8, 4x6=24, 4x7+2=30, …]
0 0 0 0 0 0 ← 0 x 234762
7 0 4 2 8 6 ← 3 x 234762
2 3 4 7 6 2 ← 1 x 234762
4 6 9 5 2 4-------- ← 2 x 234762
5 0 0 1 3 6 9 6,4 8
+ 0 1 2 3 4 5 6 7 x 0 1 2 3 4 5 6 7
0 0 1 2 3 4 5 6 7 0 0 0 0 0 0 0 0 0
1 1 2 3 4 5 6 7 10 1 0 1 2 3 4 5 6 7
2 2 3 4 5 6 7 10 11 2 0 2 4 6 10 12 14 16
3 3 4 5 6 7 10 11 12 3 0 3 6 11 14 17 22 25
4 4 5 6 7 10 11 12 13 4 0 4 10 14 20 24 30 34
5 5 6 7 10 11 12 13 14 5 0 5 12 17 24 31 36 43
6 6 7 10 11 12 13 14 15 6 0 6 14 22 30 36 44 52
7 7 10 11 12 13 14 15 16 7 0 7 16 25 34 43 52 61
Figure 3-5 - Tables d’addition et de multiplication en octal
1 1 ← reports 123
423,17 x 456
+ 1246,03 762 ← 6 x 123 [6x3=22, 6x2+2=16, 6x1+1=7]
1671,22 637
514 —
60752
Le cas de l’arithmétique binaire est particulièrement simple, puisqu’en base 2 les tables d’addition et
de multiplication se réduisent à seulement 4 cases. Voilà qui nous aurait fortement simplifié la tâche
à l’école primaire !
+ 0 1 x 0 1
0 0 1 0 0 0
1 1 10 1 0 1
Figure 3-6 - Tables d’addition et de multiplication en binaire
Exemples en binaire :
• Ajouter des zéros à gauche de la partie entière ou à droite de la partie fractionnaire d’un
nombre ne change pas sa valeur. L’inverse n’est pas vrai.
• Décaler la représentation d’un nombre d’une position vers la gauche revient à le multiplier
par la valeur de la base.
• Décaler la représentation du nombre d’une position vers la droite revient à le diviser par la
valeur de la base.
• Il est très facile de déterminer les multiples d’une puissance positive de la base.
Les multiples de 10 (101) en base 10 se terminent par 1 zéro : 10, 130, 4560…
Les multiples de 2 (21) en base 2 aussi : 102 (210), 10102 (2010), 1101102 (10610)…
Les multiples de 1000 (103) en base 10 se terminent par 3 zéros : 2000, 54000, 1324000…
Les multiples de 8 (23) en base 2 aussi : 10002 (810), 11010002 (10410), 11011110002 (88810)…
Le reste de la division euclidienne par Bn est donné par les n derniers chiffres du nombre, le
quotient par les chiffres restants.
N = an Bn + an-1 Bn-1 + … + a2 B2 + a1 B1 + a0
R = N - an Bn = an-1 Bn-1 + … + a2 B2 + a1 B1 + a0
Les puissances successives de 8 sont : 80=1, 81=8, 82=64, 83=512, 84=4096, etc. On obtient la séquence
de resserrement d’intervalle suivante :
Dans la technique du resserrement d’intervalle, chaque quotient successif est un chiffre du résultat
final, sa valeur est donc obligatoirement comprise entre 0 et B-1.
Les puissances de 2 sont : 20=1, 21=2, 22=4, 23=8, 24=16, 25=32, 26=64, 27=128, 28=256, etc.
La technique du resserrement d’intervalle est simplifiée lors d’une conversion en binaire par le fait
que le quotient de la division est obligatoirement 0 ou 1 selon que la puissance de 2 considérée et
supérieure ou non au nombre restant. En pratique, il suffit donc de soustraire des puissances de 2 du
nombre de départ jusqu’à obtenir un reste égal à 0.
où R = a0 et
Q = an Bn-1 + an-1 Bn-2 + … + a2 B1 + a1 = N’ = ( an Bn-2 + an-1 Bn-3 + … + a2 ) x B + a1
Si on appelle N’ le quotient de la division entière de N par B, le coefficient a1 est à son tour le reste
de la division entière de N’ par la base. On recommence cette opération tant que le quotient obtenu
est différent de 0.
Vous avez un doute sur l’ordre des coefficients dans le résultat final : 50468 ou 64058 ? Une
technique simple consiste à utiliser la méthode pour convertir de la base 10 vers la base
10. On voit alors immédiatement dans quel ordre les chiffres du résultat sont calculés.
La technique reste identique. Elle est toutefois simplifiée dans la mesure où le reste de la division par
2 est 0 ou 1 suivant que le dividende est pair ou impair.
Pour convertir d’une base B = 2n vers le binaire, on transforme chaque chiffre de la base B en sa
représentation en n chiffres binaires ou bits (= binary digits) et on concatène les différents morceaux.
Exemples :
Pour convertir du binaire vers une base B = 2n, on découpe le nombre binaire en tronçons de n bits
en allant de la droite vers la gauche. Chaque tronçon correspond à un chiffre en base B. Il faut
éventuellement compléter le dernier tronçon de gauche avec des 0.
Exemples :
Pour convertir entre deux bases B et B’ qui sont des puissances de 2, on procède en deux étapes :
Exemples :
1101,0112 = 1x23 + 1x22 + 0x21 + 1x20 + 0x2-1 + 1x2-2 +1x2-3 = 8+4+1+0,25+0,125 = 13,37510
CAFE,6616 = 12x163 + 10x162 + 15x16 + 14 + 6x16-1 + 6x16-2 = 51.966,398437510
Nous nous intéressons ici à la manière de convertir la partie fractionnaire d’un nombre réel en
utilisant la méthode de multiplication par la base.
Soit N = X,Y un nombre réel. On souhaite convertir la partie fractionnaire Y vers une base B.
Attention, une partie fractionnaire finie dans une base donnée peut conduire à une
partie fractionnaire qui est illimitée dans une autre base !
Un ordinateur stocke les informations en mémoire sous une forme binaire. Ainsi, la plus petite
quantité d’information représentable par un ordinateur est le bit (contraction de « binary digit ») qui
possède deux valeurs distinctes, généralement représentées par 0 et 1. Les bits sont regroupés en
séquences, dont la longueur correspond la plupart du temps à une puissance de 2 : 1, 2, 4, 8, 16, …
La quasi-totalité des puces de mémoire actuelles sont organisées sur la base d’une unité élémentaire
de stockage (cellule mémoire) dont la taille est de 8 bits. Cette quantité de mémoire porte le nom
d’octet (byte). C’est la plus petite quantité d’information accessible de manière individuelle en
mémoire. On rencontre parfois l’appellation anglaise « nibble » pour désigner un demi-octet (4 bits).
Les cellules mémoires sont à leur tour organisées en blocs d’une taille plus importante appelés mots
mémoires. La taille d’un mot mémoire n’est pas standardisée, elle peut être de 16, 32 ou 64 bits
suivant les caractéristiques du processeur utilisé.
Pour illustrer cette situation, considérons le cas d’un bit isolé. Notre seule certitude est que ce bit
peut être dans 2 états distincts et mutuellement exclusifs. Appelons ces deux états : 0 et 1. Que
signifient ces deux états ? Il est impossible de répondre à cette question sans connaître la nature de
l’information qui est représentée par ce bit. La signification de notre bit pourrait être :
• la valeur numérique 0 ou 1 ;
• la réponse oui/non à une question ;
• la valeur vrai/faux d’une condition logique ;
• l’état allumé/éteint d’une lampe ;
• le statut ouvert/fermé d’un magasin ;
• etc.
En définitive, un bit permet de représenter deux situations possibles dont l’interprétation nécessite
de connaître le contexte dans lequel l’information est utilisée. Si nous disposons de 2 bits, ceux-ci
peuvent prendre 4 configurations distinctes (00, 01, 10, 11) et représenter 4 informations
différentes. Avec 3 bits, nous avons 8 possibilités, avec 4 bits 16 possibilités, etc. Chaque bit
supplémentaire double le nombre de configurations possibles.
En toute généralité, une séquence de 𝑛 bits nous permet de représenter 2𝑛 configurations distinctes.
Inversement, pour représenter 𝑁 informations distinctes par une séquence binaire, nous avons
besoin au minimum de 𝑙𝑜𝑔2 (𝑁) bits. L’interprétation d’une séquence binaire particulière ne peut se
faire qu’en connaissant le contexte d’utilisation de cette séquence.
Par conséquent, lorsque nous nous intéressons à la représentation interne d’un type particulier
d’informations dans un ordinateur, nous devons :
• déterminer le nombre de bits nécessaires pour représenter tous les cas distincts possibles ;
• fixer les règles à utiliser pour définir quelle séquence binaire représente quelle information.
Exemple
Si nous voulons représenter les 26 lettres majuscules (A, B, C, …, Z) par un code binaire, il
nous faut au minimum 𝑙𝑜𝑔2 (26) = 4,7 → 5 bits. Avec 5 bits, nous disposons de 32
possibilités (25 ) représentées par les 32 codes binaires de 00000 à 11111.
Nous pouvons alors décider de la convention suivante : A=0, B=1, C=2, …, Z=25. Ou encore, en
binaire : A=00000, B=00001, C=00010, …, Z=11001. Les 6 combinaisons binaires de 11010
(2610) à 11111 (3110) ne sont pas utilisées.
Dans les langages de programmation, ce sont typiquement les instructions de déclaration de données
qui établissent le lien entre une zone mémoire et la signification de son contenu.
Exemples
1. L’instruction Java « int x = -10; » déclare une zone mémoire de 32 bits qui
contiendra la représentation binaire du nombre entier signé -10 suivant la convention de
représentation en complément à 2 (voir section 3.4.3.3).
2. L’instruction C « unsigned short x = 42; » déclare une zone mémoire de 16 bits qui
contiendra la représentation binaire du nombre naturel1 42 (voir section 3.4.1).
1
Rappel : un nombre naturel est un nombre entier positif ou nul.
Il existe différents systèmes de représentation décimale des nombres : BCD (Binary Coded Decimal –
Décimal Codé Binaire), code excédent -3, code 2 dans 5, biquinaire, etc. Nous illustrons uniquement
le plus répandu de ces codes, à savoir le code BCD.
Le code BCD est très simple. Il consiste à coder chaque chiffre du nombre décimal en son équivalent
binaire sur 4 bits. Le tableau suivant illustre le code résultant.
Exemple de code BCD : 48256310 → 0100 1000 0010 0101 0110 0011, c’est-à-dire aussi 48256316 !
Remarquons au passage qu’un code sur 4 bits permet de représenter 16 valeurs (cf. hexadécimal).
Or, le code BCD ne doit représenter que les 10 chiffres (0 à 9) : il y a donc 6 codes invalides qui
correspondent aux valeurs 10 à 15.
8 1 0 0 0
+ 6 0 1 1 0
14 1 1 1 0 ← code invalide !!!
0 1 1 0 Si on ajoute 6 au résultat invalide,
0 0 0 1 0 1 0 0 on obtient le code BCD correct !
1 4
Le résultat de l’addition binaire aboutit à la valeur 14 qui est un code BCD invalide puisque le chiffre
14 n’existe pas dans le code BCD. On souhaiterait obtenir à la place un résultat composé de 2
chiffres : 1 et 4. Pour y arriver, il faut ajouter 6 au résultat (6 correspond au nombre de codes
inutilisés).
1 1
5 8 3 0 1 0 1 1 0 0 0 0 0 1 1
+ 2 9 7 0 0 1 0 1 0 0 1 0 1 1 1
8 8 0 1 0 0 0 0 0 1 0 1 0 1 0 ← code invalide !!!
8 0 1 1 0 +6 0 1 1 0 +6
1 0 0 0 0 0 0 0
8 0
Le dépassement de capacité (total des deux chiffres > 9) est détecté de deux manières :
bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
valeur 1 1 0 0 1 0 1 0 1 1 1 1 1 1 1 0
hexa C A F E
bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
valeur 0 0 0 0 1 0 0 1 1 1 1 0 0 1 0 1
hexa 0 9 E 5
Dans le deuxième exemple, la représentation binaire du nombre 2533 ne nécessite que 12 bits. Les 4
bits inutilisés du mot mémoire ont pour valeur 0.
Au sein d’un mot mémoire, les bits sont conventionnellement numérotés de droite à gauche en
commençant à zéro.
Le bit 0, c’est-à-dire le bit plus à droite, est appelé bit le moins significatif (LSB – Least Significant Bit).
C’est en effet le bit qui contribue le moins à la valeur finale du nombre puisqu’il correspond au
coefficient du terme 20.
Le bit 15 (dans le cas d’un mot de 16 bits), c’est-à-dire le bit plus à gauche, est appelé bit le plus
significatif (MSB – Most Significant Bit). C’est en effet le bit qui contribue le plus à la valeur finale du
nombre puisqu’il correspond au coefficient du terme 215.
Il est possible qu’une opération arithmétique aboutisse à un résultat dont la valeur est supérieure à
la plus grande valeur représentable. On parle dans ce cas d’un dépassement de capacité (overflow).
Exemple :
• on utilise par convention le bit situé le plus à gauche dans le mot mémoire (= bit le plus
significatif) pour indiquer le signe (S) affecté à ce nombre ;
• les autres bits du mot mémoire sont utilisés pour représenter la valeur absolue du nombre.
Par convention, le signe + est représenté par le bit S à 0 et le signe - par le bit S à 1.
2
Rappel : un nombre entier relatif est un nombre entier muni d’un signe, il peut donc être positif ou négatif.
Nombre S 6 5 4 3 2 1 0
7410 0 1 0 0 1 0 1 0
-7410 1 1 0 0 1 0 1 0
Suivant la taille de la zone mémoire, l’intervalle des nombres représentables est donc :
Cet intervalle est symétrique : il y a autant de nombres positifs que de nombres négatifs.
Inconvénients
3.4.3.2 Complément à 1
Dans la représentation en complément à 1, ou complément logique, on utilise également le bit le plus
significatif comme bit de signe. La différence réside dans le fait que pour passer d’un nombre positif
à un nombre négatif, on inverse également tous les bits représentant la valeur absolue du nombre.
Cette représentation facilite grandement la réalisation des opérations arithmétiques. Nous verrons
pourquoi dans la section suivante.
Nombre S 6 5 4 3 2 1 0
7410 0 1 0 0 1 0 1 0
Complément à 1 -7410 1 0 1 1 0 1 0 1
L’intervalle des nombres représentables avec k bits est le même que précédemment et il existe
toujours 2 zéros distincts : +0 [00000000] et -0 [11111111].
3.4.3.3 Complément à 2
Le complément à 2, ou complément arithmétique, s’obtient en ajoutant +1 à la valeur du
complément à 1.
Nombre S 6 5 4 3 2 1 0
7410 0 1 0 0 1 0 1 0
Complément à 1 -7410 1 0 1 1 0 1 0 1
+1
Complément à 2 -7410 1 0 1 1 0 1 1 0
Méthode rapide : on recopie tous les bits du nombre positif en commençant par la droite jusqu’au
premier 1 inclus, après quoi on inverse tous les bits qui suivent.
• 0 1111111 = +127
• 0 0000000 = 0
• 1 1111111 = -1
• 1 0000001 = -127
• 1 0000000 = -128 (par convention)
L’intervalle des nombres représentables avec k bits va à présent de -2k-1 à +(2k-1-1). Il est asymétrique.
Exemples :
74 01001010 23 00010111
-15 +11110001 -45 +11010011
59 100111011 -22 11101010
Un dépassement de capacité est détecté par l’une ou l’autre des conditions suivantes :
• la retenue générée sur le bit de signe est différente de celle générée sur le bit juste avant ;
• les deux opérandes sont de même signe, lequel est différent de celui du résultat.
Exemples :
Pour réaliser cette opération, nous devons cependant tenir compte de la représentation utilisée :
• s’il s’agit d’un nombre naturel (non signé), il suffit d’ajouter des bits à 0 à gauche ;
• dans le cas d’un nombre entier signé représenté en signe et valeur absolue, il faut déplacer le
bit de signe dans la nouvelle position la plus à gauche (bit le plus significatif) et compléter la
valeur absolue en lui ajoutant des bits à 0 à gauche ;
• enfin, pour « agrandir » un nombre entier signé codé en complément à 2 (ou à 1), il convient
de recopier le bit de signe dans tous les bits ajoutés à gauche. Cette opération porte le nom
d’extension de signe.
Exemples
Considérons un code binaire sur 8 bits. Il permet de représenter 256 (=28 ) valeurs distinctes. Une
interprétation possible de ces 256 codes binaires est qu’ils représentent les nombres naturels de 0 à
255. Cette interprétation n’est pas obligatoire : c’est juste une possibilité parmi d’autres.
Supposons à présent que nous devions représenter des valeurs entières de températures comprises
entre -50°C et +205°C. Nous pouvons bien sûr décider d’utiliser une représentation signée en
complément à deux. Cependant, pour couvrir cette plage de valeurs, nous devons au minimum
utiliser un nombre signé sur 16 bits, ce qui représente un gaspillage de place considérable.
Une autre possibilité est de constater que notre intervalle de températures n’est rien d’autre qu’un
intervalle de 256 nombres entiers consécutifs et d’utiliser une convention appelée notation biaisée
pour les représenter en mémoire. Cette technique consiste à ajouter une quantité donnée, appelée
biais, aux nombres entiers que l’on souhaite représenter, afin de mettre l’intervalle souhaité en
correspondance avec celui des nombres naturels de 0 à 255.
Remarque : en fonction de la plage de valeurs à représenter, le biais peut être une quantité positive
ou négative. Ainsi, pour représenter l’intervalle [+723,+978], le biais à utiliser serait de -723.
Pour notre exemple des températures, nous devons utiliser un biais de +50 :
• le nombre effectif -50 est représenté par la valeur biaisée 0,
• le nombre effectif 0 est représenté par la valeur biaisée 50,
• le nombre effectif +205 est représenté par la valeur biaisée 255.
En partant du nombre effectif, il faut donc ajouter le biais pour obtenir la valeur biaisée. Celle-ci est
ensuite stockée en mémoire comme un nombre naturel, c’est-à-dire en binaire et sans bit de signe.
Exemple
T° = -15°C Valeur biaisée = -15 + 50 = 35 En mémoire : 00100011
En mémoire = 01011100 Valeur biaisée = 92 T° = 92 – 50 = 42°C
Les T° inférieures à -50°C et supérieures à +205°C ne sont pas représentables sur 8 bits !
Considérons par exemple le nombre entier non signé 271284731610 dont la représentation
hexadécimale est A1B2C3D416. La représentation de ce nombre nécessite 32 bits, c’est-à-dire 4
octets dont les valeurs hexadécimales sont respectivement A1, B2, C3 et D4. Ces 4 octets peuvent
être placés en mémoire de deux manières différentes :
La première ligne du tableau montre que les octets du mot de 32 bits sont placés en mémoire en
commençant par l’octet de poids fort « A1 » pour terminer avec l’octet de poids faible « D4 ». Cette
manière de procéder porte le nom de gros-boutiste (big-endian en anglais).
La deuxième ligne du tableau montre que les octets du mot de 32 bits sont placés en mémoire en
commençant par l’octet de poids faible « D4 » pour terminer avec l’octet de poids fort « A1 ». Cette
manière de procéder porte le nom de petit-boutiste (little-endian en anglais).
justifier la supériorité d’une approche sur l’autre. Certains processeurs, comme le processeur MIPS
dont nous parlerons au Chapitre 5, supportent même les deux modes et peuvent fonctionner dans
l’un ou dans l’autre en fonction d’un choix logiciel ou matériel. On parle alors de bi-boutisme.
Dans de nombreuses situations, le choix de l’une ou l’autre approche n’a aucune incidence sur le
programmeur puisque la représentation des données et leur stockage en mémoire sont pris en
charge de manière transparente par le compilateur et par le processeur.
La situation se complique dans des situations telles que l’échange et le traitement de fichiers de
données ou la transmission d’informations sur un réseau informatique. Une fois que les données
sont « externalisées », c’est-à-dire ne se trouvent plus dans l’environnement fermé d’une
architecture spécifique, il faut se mettre d’accord sur une convention d’ordonnancement des octets,
sous peine d’avoir des surprises.
« On appelle cela le problème NUXI, en effet si on veut envoyer la chaîne « UNIX » en regroupant deux
octets par mot entier de 16 bits sur une machine de convention différente, alors on obtient NUXI. Ce
problème a été découvert en voulant porter une des premières versions d'Unix d'un PDP-11 mi-
boutiste sur une architecture IBM gros-boutiste.
Le protocole IP définit un standard, le network byte order (soit ordre des octets du réseau). Dans ce
protocole, les informations binaires sont en général codées en paquets, et envoyées sur le réseau,
l'octet de poids le plus fort en premier, c'est-à-dire selon le mode gros-boutiste et cela quel que soit le
boutisme naturel du processeur hôte. » [ Source : https://fr.wikipedia.org/wiki/Boutisme ]
Certains algorithmes mathématiques qui traitent des séquences d’octets en les regroupant en mots
de 16, 32 ou 64 bits peuvent également imposer un boutisme particulier. Ainsi, fonction de hachage
cryptographique MD5 qui calcule l’empreinte numérique d’un fichier utilise la convention petit-
boutiste (little-endian), alors que sa remplaçante, la fonction de hachage SHA-256, utilise quant à elle
la convention gros-boutiste (big-endian). Il faut donc rester attentif à cette problématique !
Comment peut-on dès lors représenter un nombre réel en mémoire ? Nous avons vu précédemment
que 46,687510 = 101110,10112. Il nous faut donc au minimum 6 bits pour représenter la partie
entière et 4 bits pour représenter la partie décimale.
La représentation en virgule fixe consiste à définir de manière fixe le nombre de bits attribués à la
partie entière et à la partie fractionnaire d’un nombre réel. Ainsi, si on représente un nombre réel sur
32 bits, on peut décider que 16 bits sont alloués à la partie entière et 16 bits à la partie fractionnaire.
Cette manière de procéder n’est pas très flexible. Suivant le type de calculs effectués, on peut avoir
besoin d’une partie entière importante (> 16 bits) et se contenter de peu de précision dans la partie
fractionnaire, ou devoir manipuler des valeurs qui ne sont pas très grandes, mais qui demandent au
contraire une grande précision, c’est-à-dire un nombre important de décimales après la virgule.
La représentation en virgule fixe ne permet pas de satisfaire simultanément ces deux objectifs sans
devoir utiliser un espace mémoire qui devient vite déraisonnable. Ainsi, si on voulait utiliser un
format unique en virgule fixe pour représenter des longueurs en mètres aussi diverses que la
distance entre la Terre et le Soleil (±150 millions de km = 1,5 x 1011 m) ou le rayon d’un atome
d’oxygène (60 pm = 6 x 10-11 m), il faudrait utiliser 10 octets (80 bits) !
Pour toutes ces raisons, on a imaginé un système plus flexible : la représentation en virgule flottante.
Notons cependant que la représentation en virgule fixe est toujours utilisée pour certaines
applications scientifiques spécifiques, car dans certains contextes spécifiques elle peut s’avérer
beaucoup plus performante que la représentation en virgule flottante.
𝑁𝑜𝑚𝑏𝑟𝑒 = 𝑀𝑎𝑛𝑡𝑖𝑠𝑠𝑒𝑥𝐵𝑎𝑠𝑒𝐸𝑥𝑝𝑜𝑠𝑎𝑛𝑡
Ainsi, le nombre réel 1.234,567 peut aussi s’exprimer comme 1,234567 x 103.
On peut considérer que la mantisse (ou significande) donne l’information « utile », la précision, et
que l’exposant donne l’ordre de grandeur du nombre.
Nous savons également que l’espace disponible pour représenter un nombre en mémoire est limité.
On utilisera donc la notation précédente afin de mémoriser un maximum de chiffres significatifs,
c’est-à-dire apportant une information utile.
Ainsi, si on considère le nombre 0,00012, on peut le réécrire sous la forme 1,2 x 10-4. De même, pour
un grand nombre tel que 25 milliards, plutôt que d’écrire 25.000.000.000, on préférera la forme
25 x 109, ou mieux encore 2,5 x 1010. Cette dernière forme est appelée notation scientifique
normalisée.
La forme normalisée d’un nombre réel décimal s’obtient en ajustant l’exposant de telle sorte que la
partie entière de la mantisse comporte 1 seul chiffre non nul compris entre 1 et 9. En toute rigueur,
le nombre zéro ne peut donc pas être représenté de manière normalisée.
Exemple : la forme normalisée de 12,4 est 1,24 x 101. A contrario, 0,0124 x 103 n’est pas normalisé !
Parlons à présent de la représentation des nombres réels en virgule flottante telle qu’elle est définie
par le standard IEEE 754. Cette représentation consiste à stocker en mémoire la forme normalisée
d’un nombre réel de la manière suivante :
SM EXPOSANT MANTISSE
La forme normalisée d’un nombre réel binaire définie par le standard IEEE 754 correspond à celle de
la notation scientifique. Elle s’obtient en ajustant l’exposant de telle sorte que la partie entière de la
mantisse soit égale à 1, qui est la seule valeur non nulle possible en binaire. La seule exception est le
nombre 0 qui est représenté par un mot mémoire dont tous les bits sont à 0.
Le standard IEEE 754 définit le nombre de bits alloués à l’exposant et à la mantisse en fonction de la
précision choisie, ainsi que le biais appliqué à l’exposant.
En ce qui concerne l’exposant, on constate que les valeurs représentables vont de -127 (0-127) à
+128 (255-127) en simple précision et de -1023 (0-1023) à +1024 (2047-1023) en double précision.
Par ailleurs, comme le premier bit de la mantisse normalisée vaut toujours 1, il n’est pas nécessaire
de le stocker. On stocke uniquement la partie fractionnaire de la mantisse, ce qui permet de gagner
un peu de précision supplémentaire. Le traitement est en revanche plus compliqué.
Pour rappel, on ne stocke pas le bit qui correspond à la partie entière de la mantisse. Si la partie
fractionnaire de la mantisse comporte moins de 23 bits (simple précision), on complète à droite avec
des zéros. Si elle comporte plus de 23 bits, on laisse tomber les bits excédentaires, ce qui entraîne
une inévitable perte de précision.
Exemple 1 : convertir 11,2510 vers sa représentation en virgule flottante simple précision IEEE 754.
1. Signe positif → 0
2. Conversion de la partie entière : 1110 = 10112
3. Conversion de la partie fractionnaire : 0,2510 = 0,012
→ Nombre binaire = 1011,012
4. Nombre binaire normalisé : 1,01101 x 23
→ Mantisse à stocker = 01101 (on laisse tomber le 1 de la partie entière)
→ Exposant = 3
5. Exposant biaisé : 3 + 127 = 13010
6. Conversion de l’exposant biaisé : 13010 = 100000102
7. Résultat final :
S EXPOSANT MANTISSE
0 1 0 0 0 0 0 1 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Signe négatif → 1
Mantisse à stocker : 10101 (on laisse tomber le 1 de la partie entière)
Exposant biaisé : -2 + 127 = 12510 = 11111012
S EXPOSANT MANTISSE
1 0 1 1 1 1 1 0 1 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Exemple
S EXPOSANT MANTISSE
1 1 0 0 0 0 1 0 1 1 0 1 1 0 1 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0
Signe : 1 → nombre négatif
• Il existe une infinité de nombres réels, alors que la quantité de nombres en virgule flottante
est limitée par l’espace mémoire alloué à leur représentation : ±232 nombres (ou ±264).
Ainsi, si on essaie de stocker le nombre réel 390411213,16718 en float simple précision, le
nombre effectivement stocké est 390411200,0. On a donc une erreur de 13,16718 !
• Quel que soit le nombre réel choisi, il y en a toujours un qui est plus grand en valeur absolue.
La plus grande valeur absolue représentable sur 32 bits en virgule flottante est :
S EXPOSANT MANTISSE
0/1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Attention : si l’exposant vaut +128, tous les bits de la mantisse doivent être nuls. Sinon, le
nombre n’est pas valide. Il y a une erreur de type NaN ( Not a Number).
• Quel que soit le nombre réel choisi, il y en a toujours un qui est plus petit en valeur absolue.
La plus petite valeur absolue normalisée représentable sur 32 bits en virgule flottante est :
Pourquoi s’arrêter à 2-126, alors qu’il est possible de représenter un exposant égal à -127 ? Le
standard IEEE permet de gagner encore un peu de précision au moyen de nombres dits non
normalisés. Ceux-ci correspondent aux nombres de la forme ± 0, ... x 2-127. Ils sont appelés
« non normalisés », car la partie entière de la mantisse vaut 0 et pas 1. La plus petite valeur
absolue non normalisée représentable sur 32 bits en virgule flottante est par conséquent :
Pour indiquer un sous-passement de capacité, le standard IEEE 754 utilise la valeur zéro.
• Quels que soient les deux nombres réels choisis, il existe toujours un nombre réel situé entre
eux. En d’autres termes, l’écart entre deux nombres réels peut se réduire indéfiniment, ce
qui se traduit par un nombre potentiellement infini de chiffres dans la partie fractionnaire.
En virgule flottante, le nombre de chiffres significatifs est limité par l’espace mémoire alloué
à la mantisse. L’écart minimum entre deux nombres consécutifs est donc fonction de la taille
de la mantisse, mais aussi de l’exposant :
± 0,00000000000000000000001 x 2k
Toutes ces différences expliquent que le calcul numérique en virgule flottante n’est pas équivalent au
calcul sur des nombres réels. Il peut conduire à des erreurs catastrophiques si on n’y prête pas
attention. L’analyse numérique est une discipline qui s’intéresse à cette problématique et qui vise à
concevoir des algorithmes capables de résoudre numériquement des problèmes de mathématiques
continues avec une précision satisfaisante.
Très vite, il a été nécessaire de représenter d’autres informations que des nombres. On a donc
inventé des codes alphanumériques qui permettent de représenter des chiffres, des lettres, des
signes de ponctuation, des opérateurs arithmétiques, etc.
Dans un code alphanumérique, la correspondance entre un caractère et son code est donnée par une
table de codage qui est propre au code considéré.
Nous présentons brièvement ci-après trois codes alphanumériques représentatifs de l’évolution des
techniques de codage : EBCDIC, ASCII et Unicode.
Code Nom Code Nom Code Car Code Car Code Car Code Car Code Car Code Car
0 NUL 16 DLE 32 Esp. 48 0 64 @ 80 P 96 ` 112 p
1 SOH 17 DC1 33 ! 49 1 65 A 81 Q 97 a 113 q
2 STX 18 DC2 34 " 50 2 66 B 82 R 98 b 114 r
3 ETX 19 DC3 35 # 51 3 67 C 83 S 99 c 115 s
4 EOT 20 DC4 36 $ 52 4 68 D 84 T 100 d 116 t
5 ENQ 21 NAK 37 % 53 5 69 E 85 U 101 e 117 u
6 ACK 22 SYN 38 & 54 6 70 F 86 V 102 f 118 v
7 BEL 23 ETB 39 ' 55 7 71 G 87 W 103 g 119 w
8 BS 24 CAN 40 ( 56 8 72 H 88 X 104 h 120 x
9 HT 25 EM 41 ) 57 9 73 I 89 Y 105 i 121 y
10 LF 26 SUB 42 * 58 : 74 J 90 Z 106 j 122 z
11 VT 27 ESC 43 + 59 ; 75 K 91 [ 107 k 123 {
12 FF 28 FS 44 , 60 < 76 L 92 \ 108 l 124 |
13 CR 29 GS 45 - 61 = 77 M 93 ] 109 m 125 }
14 SO 30 RS 46 . 62 > 78 N 94 ^ 110 n 126 ~
15 SI 31 US 47 / 63 ? 79 O 95 _ 111 o 127 DEL
Les codes 0 à 31 correspondent à des caractères de contrôle qui sont utilisés pour effectuer la
signalisation lors de la transmission de données ou pour contrôler l’affichage sur un terminal
(tabulation, retour à la ligne, …).
D’autre part, les caractères accentués ne sont pas identiques en français (é, è, à, …), en allemand (ü,
ä, …) ou en espagnol (ñ, …). Les langues scandinaves utilisent des caractères additionnels tels que le
ø. Certains pays utilisent un alphabet totalement différent du nôtre : cyrillique, arabe, japonais, etc.
Enfin, des usages particuliers nécessitent des alphabets ou des caractères spécifiques : smileys,
alphabet morse, notes de musique, alphabet braille…
Pour toutes ces raisons, l’organisation internationale de normalisation (ISO) d’une part et un
groupement d’entreprises du secteur informatique d’autre part, ont décidé de définir un jeu de
caractères universel et extensible : le standard UNICODE.
L’idée initiale d’Unicode était d’assigner à chaque caractère un code unique (point de code - code
point) sur 16 bits. Chaque caractère possède un code de taille fixe, ce qui facilite l’écriture des
logiciels. Les codes 0 à 255 correspondent aux caractères du code ASCII, ce qui permet une transition
aisée vers Unicode.
symboles (idéogrammes) différents. L’alphabet japonais kanji complet contient à lui seul plus de
50.000 idéogrammes !!!
Il a donc fallu étendre l’espace de codage d’Unicode qui contient actuellement 17 plans de 65.536
caractères chacun. Soit un total de 1.114.112 caractères dont environ 138.000 sont déjà alloués dans
la version 13.0 du standard, publiée en mars 2020.
Il existe différentes représentations internes des points de code Unicode. Une des représentations
très répandues, notamment sur Internet, est appelée UTF-8 (UTF = Unicode Transformation Format).
UTF-8 représente un caractère Unicode sur 1 à 4 octets.
Une caractéristique intéressante de la représentation UTF-8 est qu’elle est rétrocompatible avec le
code ASCII. Les caractères ASCII de 0 à 127 (bit de poids fort = 0) sont représentés tels quels, sur 1
octet. Si le bit de poids fort du premier octet est mis à 1, cela signifie que le caractère est stocké sur
plusieurs octets. Pour obtenir plus de détails sur le codage UTF-8, consultez un des nombreux sites
Internet consacrés à ce sujet.
Chaque langage peut utiliser ses propres conventions pour représenter une donnée en mémoire.
Ainsi, le langage C ne définit pas de type booléen, mais il considère qu’une valeur numérique nulle
signifie faux et qu’une valeur numérique non nulle signifie vrai.
Le langage C stocke une chaîne de caractères en mémoire comme une succession de caractères
terminée par un caractère spécial qui en indique la fin. Le langage Java, quant à lui, n’utilise pas de
délimiteur de fin de chaîne, mais il stocke explicitement la longueur de celle-ci.
Toutes ces spécificités seront abordées dans d’autres cours (Programmation de base, Structures
avancées de données…).
Les puces mémoires ne sont pas infaillibles. Les supports de stockage (disques durs, clés USB) non
plus. Il est donc possible que des erreurs apparaissent dans les données stockées. De même, des
phénomènes parasites peuvent introduire des erreurs (voire des pertes) de données lors d’une
transmission sur un réseau.
La valeur du bit de parité est calculée afin que le nombre total de bits à 1 soit pair (parité paire) ou
impair (parité impaire). Supposons par exemple que le texte « HELLO » encodé en ASCII sur 7 bits
doit être transmis entre deux ordinateurs. Voici le code résultant :
Supposons que l’on utilise un contrôle de parité pair et que le caractère ‘E’ soit modifié durant la
transmission en [11000001]. Le récepteur constatera que le nombre de bits à 1 du deuxième
caractère n’est plus pair. Il en déduit qu’une erreur s’est produite.
Le contrôle de parité (pair ou impair) permet de détecter une erreur lorsqu’un nombre impair de bits
sont en erreur. En revanche, il ne permet ni de détecter un nombre pair de bits en erreur ni de
déterminer où se trouvent les erreurs. Dans le cas d’une transmission de données, il faudra envoyer à
nouveau les données erronées.
Le contrôle au moyen d’un bit de parité convient uniquement dans des circonstances où le taux
d’erreur et la probabilité d’erreurs multiples sont très faibles.
Dans l’exemple ci-dessous, le contrôle horizontal utilise un bit de parité pair (PP), le contrôle vertical
utilise un bit de parité impair (PI). Le bit 2 du caractère ‘E’ est en erreur, comme précédemment.
Car. b6 b5 b4 b3 b2 b1 b0 PP
H 1 0 0 1 0 0 0 0
E 1 0 0 0 0 0 1 1
L 1 0 0 1 1 0 0 1
L 1 0 0 1 1 0 0 1
O 1 0 0 1 1 1 1 1
PI 0 1 1 1 1 0 1
Le contrôle des parités indique une erreur dans le deuxième caractère et dans la colonne ‘b2’. C’est
donc le bit 2 du caractère ‘E’ qui est en erreur et nous pouvons le corriger.
Le système de la double parité permet de détecter un nombre impair d’erreurs dans une ligne ou
dans une colonne. Il permettra également de corriger un nombre impair d’erreurs, si elles
surviennent toutes dans la même ligne ou dans la même colonne.
Il existe de nombreuses variantes plus complexes du code de Hamming. Elles sortent du cadre de ce
cours.
Imaginons que nous souhaitions transmettre une donnée de d bits. Nous allons lui ajouter une
information de contrôle de k bits qui nous permettra de détecter et de corriger 1 bit en erreur.
L’information de contrôle doit nous permettre d’identifier l’absence d’erreur ou celui des m bits du
message qui est erroné, ce qui nous donne m+1 possibilités : pas d’erreur, bit 1, bit 2, … bit m. Le
nombre de bits de contrôle k doit donc être tel que 2k ≥ m+1. Idéalement, afin d’éviter le gaspillage,
on s’arrangera pour que la taille du message soit égale à m = 2k-1, c’est-à-dire d = 2k-1-k ce qui
correspond au maximum de bits de données utiles pour une valeur de k donnée.
Exemples :
Si on numérote les bits du code de Hamming de droite à gauche en commençant par 1, les bits de
contrôle sont ceux dont le numéro correspond à une puissance de 2, à savoir : 1, 2, 4, 8, 16, … Les
autres positions sont occupées par les bits de données.
7 6 5 4 3 2 1
d4 d3 d2 k3 d1 k2 k1
Chaque bit de contrôle ki effectue un test de parité sur une partie des bits mj du message. Pour
déterminer qui contrôle quoi, il suffit de convertir le numéro de chaque bit en binaire :
2. Établir la structure du message : les k bits de contrôle occupent les positions qui
correspondent aux k premières puissances de 2 (1, 2, 4, 8…), les autres positions sont
occupées par les bits de données.
3. Calculer la valeur de chaque bit de contrôle. Pour déterminer la valeur du bit k1, on compte le
nombre de bits à 1 parmi les bits 3, 5 et 7 du message et on ajuste la valeur de k1 de manière
à obtenir un nombre pair ou impair de 1 en fonction la parité choisie. Idem pour le bit k2 (bits
3, 6 et 7) et le bit k3 (bits 5, 6, 7).
Exemple : calculer le code de Hamming à parité paire pour protéger la donnée de 4 bits « 1011 ».
Pour protéger 4 bits de données, il faut ajouter 3 bits de contrôle. La longueur totale du code
de Hamming est donc égale à 7 bits. La structure du message est donnée ci-dessous : les bits
de contrôle occupent les positions 1, 2 et 4 (puissances de 2), les 4 bits de données occupent
les 4 autres positions (3, 5, 6 et 7).
7 6 5 4 3 2 1
1 0 1 k3 1 k2 k1
• k1 (bits 3, 5, 7) : 3 bits à 1 → k1 = 1
• k2 (bits 3, 6, 7) : 2 bits à 1 → k2 = 0
• k3 (bits 5, 6, 7) : 2 bits à 1 → k3 = 0
7 6 5 4 3 2 1
1 0 1 0 1 0 1
2. Calculer la valeur de chaque bit de contrôle ki’ sur base du contenu message reçu.
3. Comparer la valeur calculée ki’ avec la valeur du bit ki dans le message reçu. Si les deux
valeurs sont identiques le résultat Ri vaut 0, sinon Ri vaut 1.
4. Le nombre binaire Ri … R2R1 vaut 0 s’il n’y a pas d’erreur. Sinon, sa valeur décimale indique le
numéro du bit en erreur.
Exemple : vérifier si le code de Hamming à parité paire « 1110101 » est correct et récupérer les
données protégées par ce code. [Note : il s’agit du code de Hamming de l’exemple précédent dans
lequel on a modifié le bit 6 de 0 à 1.]
La longueur totale du message étant de 7 bits, il contient 3 bits de contrôle aux positions 1, 2
et 4 (puissances de 2) et par conséquent 4 bits de données aux positions 3, 5, 6 et 7. La
structure du code est la suivante :
7 6 5 4 3 2 1
1 1 1 0 1 0 1
d4 d3 d2 k3 d1 k2 k1
Calculons la valeur présumée ki’ de chaque bit de contrôle de parité paire sur la base du
contenu du message reçu et comparons cette valeur à la valeur ki effectivement reçue :
• k1’ (bits 3, 5, 7) : 3 bits à 1 → k1’ = 1 k1 = 1 → correct : R1 = 0
• k2’ (bits 3, 6, 7) : 3 bits à 1 → k2’ = 1 k2 = 0 → erreur : R2 = 1
• k3’ (bits 5, 6, 7) : 3 bits à 1 → k3’ = 1 k3 = 0 → erreur : R3 = 1
Pour obtenir les bits de données transmis, nous commençons par corriger le bit 6 qui passe
de 1 à 0, puis nous supprimons les bits de contrôle. La donnée reçue est donc « 1011 »
Exemple : protéger la donnée de 11 bits « 01101001101 » avec un code de Hamming à parité paire.
Pour protéger 11 bits de données, il faut utiliser 4 bits de contrôle qui occuperont les
positions 1, 2, 4 et 8. Le message de 15 bits à transmettre est donc de la forme :
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
0 1 1 0 1 0 0 k4 1 1 0 k3 1 k2 k1
Les bits de données à 1 occupent les positions 14, 13, 11, 7, 6 et 3. Pour déterminer la valeur
des 4 bits de contrôle du code de Hamming, il suffit de convertir ces positions en binaire sur
4 bits et de calculer la valeur du bit de parité paire pour chaque colonne.
14 1110
13 1101
11 1011
7 0111
6 0110
3 0011
1 0 1 0 → k4=1, k3=0, k2=1, k1=0 (pour une parité impaire, il faut inverser.)
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
0 1 1 0 1 0 0 1 1 1 0 0 1 1 0
La technique la plus utilisée est celle du CRC (Cyclic Redundancy Check) ou contrôle de redondance
cyclique.
L’idée générale de cette technique (que nous ne détaillerons pas) est la suivante :
• Il multiplie le polynôme M(x) par xr, ce qui revient à ajouter r zéros à la fin du message de
départ.
• Le polynôme transmis T(x) est le résultat de M(x).xr – R(x) (on soustrait le reste).
• A la réception, le message reçu est divisé par le même polynôme générateur G(x).
• Si le reste de la division n’est pas nul, c’est qu’une erreur de transmission s’est produite.
Grâce aux CRC, on peut détecter des erreurs groupées affectant jusqu’à 16 bits consécutifs ou plus !
De nombreuses méthodes (algorithmes) de compression existent. Elles se différencient les unes des
autres par les caractéristiques suivantes :
• Le taux de compression – C’est le rapport entre la taille des données initiales et la taille des
données compressées. Les algorithmes sans perte arrivent à des taux de compression
moyens de l’ordre de 3:1. Les algorithmes avec perte peuvent atteindre 10:1, voire 100:1.
• Usage général: Codage Huffman, Lempel-Ziv-Welsch (LZW), Run Length Encoding (RLE), …
• Audio : Apple Lossless (ALAC), Dolby TrueHD, WMA Lossless, …
• Image : GIF, PNG, TIFF
Une fonction logique associe à une ou plusieurs variables logiques d’entrée un résultat dont la valeur
est elle aussi 0 ou 1. Cette valeur dépend de l’état des variables d’entrée.
On décrit généralement une fonction logique au moyen d’une table de vérité. Cette table indique la
valeur de la fonction logique pour toutes les combinaisons possibles des variables d’entrée.
Pour n variables logiques, le nombre de combinaisons possibles est égal à 2n. Pour chacune de ces
combinaisons, la fonction logique peut prendre deux valeurs possibles (0 ou 1). Il y a donc au total
𝑛
22 fonctions logiques distinctes de n variables, ce qui donne :
• 1 variable : 4 fonctions,
• 2 variables : 16 fonctions,
• 3 variables : 256 fonctions,
• Etc.
a
0 1 f(a)
0 0 0 Constante 0
1 0 1 Identité
2 1 0 Complémentation : 𝑎̅ (NOT)
3 1 1 Constante 1
Table 4.1 - Fonctions logiques d'une variable
Parmi ces différentes fonctions, la seule qui soit vraiment intéressante est la fonction de
complémentation (3ème ligne de la table). On parle d’opérateur NON (NOT) ou encore d’inverseur, car
la fonction inverse la valeur de la variable d’entrée.
ab
00 01 10 11 f(ab)
0 0 0 0 0 0 (constante)
1 0 0 0 1 𝑎𝑏 (AND)
2 0 0 1 0 𝑎𝑏̅
3 0 0 1 1 𝑎
4 0 1 0 0 𝑎̅𝑏
5 0 1 0 1 𝑏
6 0 1 1 0 𝑎 ⊕ 𝑏 = 𝑎𝑏 + 𝑎𝑏 (XOR)
7 0 1 1 1 𝑎 + 𝑏 (OR)
8 1 0 0 0 ̅̅̅̅̅̅̅
𝑎 + 𝑏 = 𝑎 ↓ 𝑏 (NOR)
9 1 0 0 1 ̅̅̅̅̅̅̅̅
𝑎 ⊕ 𝑏 = 𝑎 ⇔ 𝑏 (XNOR)
10 1 0 1 0 𝑏̅
11 1 0 1 1 𝑎 + 𝑏̅
12 1 1 0 0 𝑎̅
13 1 1 0 1 𝑎̅ + 𝑏
14 1 1 1 0 ̅̅̅ = 𝑎 ↑ 𝑏 (NAND)
𝑎𝑏
15 1 1 1 1 1 (constante)
Table 4.2 - Fonctions logiques de deux variables
Certains circuits électroniques travaillent en logique positive, c’est-à-dire qu’une tension de 0 volt
correspond à la valeur logique 0 et une tension positive (par exemple 1,5 volt) correspond à la valeur
logique 1. D’autres travaillent en logique négative, c’est-à-dire que 0 volt représente la valeur logique
1 et 1,5 volt représente la valeur logique 0. Sauf mention contraire explicite, nous utiliserons toujours
une logique positive.
Nous donnons ci-après les tables de vérité, les noms (français et anglais) et les symboles des
principaux opérateurs logiques. Dans ce syllabus, nous utiliserons indifféremment les noms français
et anglais.
Il convient de signaler que l’opérateur logique ET/AND est soit représenté par un point ‘.’, soit omis
s’il n’y a pas de confusion possible, ainsi : « abc = a.b.c = ab.c = a.bc ».
NON / NOT
a not
0 1 a a
1 0 NOT
ET / AND
a b and
0 0 0 a ab
0 1 0 b
1 0 0 AND
1 1 1
OU / OR
a b or
0 0 0 a
0 1 1 a+b
b
1 0 1 OR
1 1 1
OU EXCLUSIF / XOR
a b xor
0 0 0 a
0 1 1 a⊕b
b
1 0 1
XOR
1 1 0
NON-ET / NAND
a b nand
0 0 1
0 1 1
a ab = a b
1 0 1 b
1 1 0 NAND
NON-OU / NOR
a b nor
0 0 1
a
0 1 0 a+b = a b
1 0 0 b
1 1 0 NOR
a b xnor
0 0 1 a
0 1 0 a b
1 0 0 b
1 1 1 XNOR
On peut par exemple réaliser les fonctions ET et OU en plaçant des interrupteurs électriques
respectivement en série ou en parallèle, comme illustré ci-dessous.
ET OU
Dans le circuit de gauche, la lampe s’allume uniquement si les deux interrupteurs sont fermés (ET).
Dans le circuit de droite, la lampe s’allume si au moins un des deux interrupteurs est fermé (OU).
Les portes logiques des circuits électroniques sont quant à elles généralement construites à partir
d’un composant de base appelé transistor. Un transistor fonctionne essentiellement comme un
interrupteur. Le schéma suivant illustre son fonctionnement.
Le tableau ci-dessous illustre la manière de créer des portes logiques AND, OR, NAND et NOR avec
des transistors. Comme dans le cas des circuits électriques qui utilisent des interrupteurs, on
retrouve un montage en série pour réaliser une porte AND et un montage en parallèle pour réaliser
une porte OR. Les portes NAND et NOR diffèrent juste par le fait qu’on a déplacé le point de sortie et
la résistance en amont des transistors, comme dans le cas de l’inverseur.
Étant donné une fonction logique de n variables, on appelle minterme un produit logique (ET) où ces
n variables apparaissent soit sous leur forme directe (𝑎), soit sous leur forme complémentée (𝑎̅). De
même, on appelle maxterme une somme logique (OU) de ces n variables.
Partant de la table de vérité d’une fonction logique, il est possible de formuler algébriquement cette
fonction sous deux formes particulières appelées formes normales.
• Somme logique (OU) des mintermes (produits logiques - ET) correspondant aux lignes de la
table de vérité pour lesquelles la fonction vaut 1.
• Dans l’expression d’un minterme, une variable d’entrée est complémentée si sa valeur est 0.
a b xor a ab
0 0 0 𝑎⊕𝑏 b
0 1 1 a⊕b
1 0 1 𝐹𝑁1 = 𝑎̅𝑏 + 𝑎𝑏̅
1 1 0
ab
L’interprétation de la première forme normale est assez intuitive. Le XOR vaut 1 si a=0 et b=1 (𝑎̅𝑏 est
vrai) OU si a=1 et b=0 (𝑎𝑏̅ est vrai), c’est-à-dire 𝑎̅𝑏 + 𝑎𝑏̅.
• Produit logique (ET) des maxtermes (sommes logiques - OU) correspondant aux lignes de la
table de vérité pour lesquelles la fonction vaut 0.
• Dans l’expression d’un maxterme, une variable d’entrée est complémentée si sa valeur est 1.
Les règles à appliquer pour obtenir la deuxième forme normale correspondent à « l’inverse » de
celles de la première forme normale : opérateurs ET ↔ OU, lignes 0 ↔ 1, variables 𝑎 ↔ 𝑎.
a b xor a a+b
0 0 0 𝑎⊕𝑏 b
0 1 1 a b
1 0 1 𝐹𝑁2 = (𝑎 + 𝑏)(𝑎̅ + 𝑏̅)
1 1 0
a+b
La deuxième forme normale, bien qu’a priori moins évidente à interpréter, s’explique grâce au
théorème de De Morgan. En effet, le XOR vaut 0 si a=0 et b=0 (𝑎𝑏 est vrai) OU si a=1 et b=1 (𝑎𝑏 est
vrai), c’est-à-dire 𝑎𝑏 + 𝑎𝑏. Donc, le XOR vaut 1 si on prend la négation de cette formule.
En appliquant le théorème de De Morgan, obtient la deuxième forme normale :
𝑎 ⊕ 𝑏 = (𝑎𝑏 + 𝑎𝑏) = (𝑎𝑏). (𝑎𝑏) = (𝑎 + 𝑏)(𝑎̅ + 𝑏̅) = (𝑎 + 𝑏)(𝑎̅ + 𝑏̅)
• Chaque minterme est réalisé par une porte logique ET à laquelle sont raccordés les n signaux
d’entrée de la fonction, éventuellement complémentés.
• Les sorties de ces portes ET sont combinées au moyen d’une porte logique OU afin de
réaliser la somme des mintermes.
• Chaque maxterme est réalisé par une porte logique OU à laquelle sont raccordés les n
signaux d’entrée de la fonction, éventuellement complémentés.
• Les sorties de ces portes OU sont combinées au moyen d’une porte logique ET afin de
réaliser le produit des maxtermes.
Nous souhaitons créer un circuit logique composé exclusivement de portes {NON, ET, OU} qui
réalise la fonction logique de 3 variables décrite par la table de vérité suivante.
a b c MAJ
0 0 0 0
0 0 1 0
0 1 0 0
0 1 1 1
1 0 0 0
1 0 1 1
1 1 0 1
1 1 1 1
Il s’agit de la fonction « majorité », car sa valeur vaut 1 lorsqu’une majorité des 3 variables
d’entrée valent 1.
Les schémas logiques des circuits correspondant à ces 2 formes normales sont présentés ci-
après. À gauche, celui qui correspond à la première forme normale (somme de mintermes). À
droite, celui qui correspond à la deuxième forme normale (produit de maxtermes).
ABC A+B+C
A A
ABC A+B+C
B B
MAJ MAJ
ABC A+B+C
C C
ABC A+B+C
La technique de synthèse d’un circuit logique présentée dans l’exemple précédent est très facile à
mettre en œuvre et facilement généralisable à n variables.
En revanche, elle ne conduit pas nécessairement au circuit logique qui utilise le moins de portes
logiques. Ainsi, si on applique la méthode de simplification de Karnaugh (cf. cours de
mathématiques) à la fonction majorité, on obtient :
𝑚𝑎𝑗(𝑎, 𝑏, 𝑐) = 𝑎𝑏 + 𝑎𝑐 + 𝑏𝑐
La mise en œuvre de cette forme simplifiée ne nécessite que 3 portes ET à 2 entrées et une porte OU
à 3 entrées.
En effet, le théorème de De Morgan nous montre qu’il est possible d’exprimer la fonction ET avec les
opérateurs {OU, NON}, ou d’exprimer la fonction OU avec les opérateurs {ET, NON}. Nous pouvons
donc exprimer toute fonction logique avec seulement besoin de 2 opérateurs logiques.
Nous pouvons aller encore plus loin et montrer que les opérateurs ET, OU, NON peuvent à leur tour
être exprimés grâce à un seul opérateur : soit NAND, soit NOR.
Les opérateurs NAND et NOR sont appelés opérateurs complets, car chacun d’eux permet de générer
tous les autres opérateurs logiques, comme illustré dans le tableau ci-dessous.
a ab = a b a
a+b = a b
b b
NAND NOR
NON ET OU
a a
a a ab a+b
NOT
b b
AND OR
a
a
a a ab a+b
b
b
𝑎 ↑ 𝑎 = 𝑎. 𝑎 = 𝑎 𝑎𝑏 = 𝑎. 𝑏 = 𝑎 ↑ 𝑏 𝑎 + 𝑏 = 𝑎 + 𝑏 = 𝑎. 𝑏 = 𝑎 ↑ 𝑏
a
a a a a+b
ab
b
b
C’est par exemple le cas pour un circuit qui implémente une fonction logique définie par une table de
vérité. Tous les circuits illustrés précédemment dans ce syllabus présentent cette caractéristique et
sont donc des circuits combinatoires.
Il existe toutefois des circuits qui n’ont pas cette caractéristique. Considérons un circuit mémoire : sa
valeur de sortie dépend bien évidemment de l’adresse mémoire demandée (entrée), mais elle
dépend surtout de la valeur qui a été précédemment stockée à cette adresse (historique du circuit).
Pour une même adresse (entrée), nous obtiendrons des valeurs différentes (sortie) au fil du temps.
4.2.2 Décodeur
Un décodeur est un circuit combinatoire dont la caractéristique est d’activer une de ses 2n sorties (S0,
S1, …) en fonction de la valeur du code appliqué à ses n lignes de commande (A, B, …). On parlera
ainsi de décodeur « 1 vers 2 » (code de 1 bit), « 2 vers 4 » (code de 2 bits), « 3 vers 8 » (code de 3
bits), etc.
DECODER
AB
00 S0
AB
01 S1
AB
10 S2
AB
11 S3
A B A B
Nous verrons qu’un circuit décodeur a de multiples usages dans un ordinateur. Il peut par exemple
être utilisé dans une unité arithmétique et logique pour sélectionner l’opération à réaliser parmi 2n
opérations possibles, sur base de n bits présents dans l’instruction à exécuter. Il permet également
de décoder une adresse de 32 bits pour aller chercher une donnée dans une mémoire de 4 GB (232).
4.2.3 Multiplexeur
4.2.3.1 Fonctionnement
Un multiplexeur est un circuit dont le rôle consiste à aiguiller le signal appliqué à l’une de ses
multiples entrées de données vers son unique sortie. La ligne de donnée à sélectionner est
déterminée par les signaux appliqués aux entrées de commande.
Le schéma interne du multiplexeur illustré à la figure ci-dessous est assez proche de celui du
décodeur. On y retrouve notamment le principe d’activation (partielle) d’une porte ET en fonction du
code appliqué aux lignes de commande. Deux éléments viennent s’ajouter :
• chaque entrée de données Dx vient se connecter sur la porte ET qui correspond au code de
sélection AB = x ;
• une porte OU combine les sorties des portes ET pour calculer la valeur de l’unique sortie du
circuit.
D0 D0AB MUX
D0 00
D1 D1AB
S
D1 01
D2
S
D2AB D2 10
D3 D3 11
D3AB
A B A B
Exemple
Pour implémenter une fonction logique à 2 variables, on choisira un multiplexeur « 4 vers 1 » avec 2
bits de commande. Pour 3 variables, on choisira un multiplexeur « 8 vers 1 » avec 3 bits de
commande. Etc.
On câble ensuite chaque entrée de données Dx en fonction de la valeur de la table de vérité pour la
combinaison ab = x :
D0
a b xor Dx
0 0 0 D0
D1
0 1 1 D1
A⊕B
1 0 1 D2
D2
1 1 0 D3
D3
A B
4.2.4 Démultiplexeur
Un démultiplexeur réalise l’inverse d’un multiplexeur.
Il possède une seule ligne d’entrée de données (E) qui est aiguillée vers une des 2n sorties (S0, S1, …)
en fonction de la valeur sélectionnée par les n lignes de commande (A, B, …).
Le schéma interne du multiplexeur illustré à la figure ci-dessous est quasiment identique à celui du
décodeur :
EAB DEMUX
00 S0
EAB
E 01 S1
E
EAB
10 S2
EAB
11 S3
A B A B
Fonctionnement : en fonction du code AB de commande, une seule porte ET de sortie est pré-
activée. Sa valeur de sortie est alors égale à 𝐸. 1. 1 = 𝐸. La valeur de sortie des trois autres portes ET
vaut 0.
Un décodeur est un cas particulier de démultiplexeur dont le signal d’entrée E vaut toujours 1
4.2.5 Comparateur
Un comparateur est un circuit combinatoire qui permet de comparer l’égalité entre 2 nombres
appliqués en entrée. Sa sortie vaut 1 si les 2 nombres sont égaux et 0 s’ils diffèrent.
Exercice : concevoir un circuit logique qui teste l’égalité de deux nombres de 4 bits.
Suggestion : commencez par concevoir un circuit logique qui compare 2 nombres de 1 bit,
puis généralisez la solution à 2, 3, 4 bits ou plus.
4.2.6 Additionneur
L’opération arithmétique binaire la plus élémentaire est l’addition.
Dans un premier temps, nous allons limiter notre ambition à l’addition de deux nombres a et b
exprimés sur 1 bit. Nous pouvons spécifier le résultat de l’addition binaire de deux nombres de 1 bit
au moyen de la table de vérité suivante :
a b S R
0 0 0 0
0 1 1 0
1 0 1 0
1 1 0 1
xor and
S est la somme binaire et R la retenue éventuelle (« carry » en anglais). On voit que la somme est le
résultat de l’opération logique 𝑎 ⊕ 𝑏 (XOR), tandis que la retenue est le résultat de l’opération
logique 𝑎𝑏 (AND).
a
S = a⊕b
b
R = ab
Un tel circuit est appelé demi-additionneur. En effet, il peut générer un bit de retenue résultant de
l’addition de a et b, mais il ne tient pas compte d’une retenue précédente éventuelle.
retenue précédente
R
S
a
b
retenue
propagée
R
retenue générée
Il y a une retenue finale R’ quand l’un ou l’autre des cas suivants survient :
Le calcul de la retenue finale est effectué par la moitié inférieure du circuit (2 portes ET + porte OU).
On peut représenter un additionneur complet par le schéma bloc suivant, qui masque les détails de
son implémentation.
data
add
Rout Rin
sum
Pour réaliser une addition sur n bits, il suffit de mettre en parallèle n additionneurs complets sur 1
bit. On connecte la sortie R’ d’un additionneur à l’entrée R de l’additionneur suivant.
a7 b 7 a2 b2 a1 b1 a0 b0
s7 s2 s1 s0
Cette approche très simple présente néanmoins un inconvénient majeur : pour obtenir le résultat de
l’addition, il faut attendre que la retenue se propage de proche en proche. On parle d’ailleurs
d’additionneur à propagation de retenue (« ripple-carry adder »). Cette propagation introduit un
délai qui devient vite pénalisant si le nombre de bits à additionner est grand. Dans la pratique, les
ordinateurs utilisent des additionneurs plus complexes et plus rapides. Un exemple d’une
amélioration possible est l’additionneur à retenue anticipée (« carry look-ahead adder »)3.
On peut représenter un additionneur sur 8 bits au moyen du schéma bloc simplifié suivant :
A Input B Input
8 8
a7...a1a0 b7...b 1b0
Carry out 8-bits adder Carry in
s7...s 1s0
8
Sum
Figure 4-12 - Schéma bloc d'un additionneur sur 8 bits
3
Voir par exemple : https://en.wikipedia.org/wiki/Carry-lookahead_adder.
Nous disposons à présent de tous les éléments nécessaires pour construire une unité arithmétique et
logique (ALU) simplifiée. Notre objectif est de concevoir un circuit logique qui pourra réaliser au choix
une des quatre opérations suivantes : ET, OU, NON, addition.
Dans un premier temps, nous considérons une ALU permettant de traiter des données sur 1 bit.
Le schéma logique détaillé ci-dessous présente une organisation possible d’une telle ALU. En
analysant ce circuit, nous pouvons constater qu’il est essentiellement composé de 3 zones
(entourées par des pointillées) : une unité logique qui réalise les opérations logiques (𝐴𝐵, 𝐴 + 𝐵, 𝐵),
une unité arithmétique composée d’un additionneur complet et un décodeur qui sélectionne
l’opération à réaliser en fonction d’un code d’opération (opcode) « F0F1 ».
AB
A+B
S
Unité logique
A
Somme
B
Décodeur
Additionneur
F0 F1 R’
En y regardant de plus près, on constate que L’ALU réalise simultanément les 4 opérations possibles
sur A et B. En fonction du code de l’opération choisie, le décodeur va activer la porte ET située à la
sortie du morceau de circuit adéquat et donc aiguiller le résultat de l’opération choisie vers la sortie
de l’ALU. Dans le cas d’une addition, le décodeur active également la sortie R’.
Chaque opération logique ou arithmétique est réalisée par une partie spécialisée de l’ALU. Dans
notre exemple très simple, les opérations logiques sont obtenues au moyen de simples portes
logiques, l’addition est quant à elle réalisée au moyen de l’additionneur complet présenté
précédemment.
Comme nous l’avons fait dans le cas d’un additionneur sur n bits, il est possible de connecter entre-
elles n ALU sur 1 bit pour obtenir une ALU sur n bits (8, 16, 32, 64 bits).
A0 B 0 A1 B 1 A 2 B2 A 3 B3
A B A B A B A B
Notre unité arithmétique et logique de base peut bien entendu être complétée de manière à
proposer d’autres opérations : XOR, multiplication, division, calculs en virgules flottantes, etc.
L’organisation générale reste la même : il y aura davantage de circuits de calcul et un décodeur plus
important (3 bits = 8 opérations, 4 bits = 16 opérations, …).
D’une manière générale, quel que soit le circuit électronique, il s’écoule toujours un certain laps de
temps entre le moment où l’on applique des signaux à l’entrée du circuit et celui auquel une réponse
stable est disponible en sortie. Ce délai est lié à la vitesse de propagation des signaux électriques, de
changement d’état des composants électroniques, etc.
Plutôt que de laisser les signaux se propager librement d’un composant à l’autre, il peut être
souhaitable de contrôler l’ordre dans lequel les événements se produisent au sein d’un circuit
électronique et à quel moment un changement d’état survient.
4.4.2 Horloge
Pour cadencer les opérations au sein d’un circuit électronique, on utilisera un signal spécial appelé
signal d’horloge. Ce signal sert à synchroniser le fonctionnement des composants électroniques sur
une référence de temps.
Dans le cadre des circuits électroniques, une horloge est un circuit électronique particulier de type
oscillateur, qui génère un signal composé d’impulsions de largeur fixe et espacées de manière
régulière.
La fréquence des impulsions est appelée fréquence d’horloge (clock frequency). Le temps qui sépare
deux impulsions (= inverse de la fréquence ou période) est appelé cycle d’horloge (clock cycle time).
Ainsi, une horloge qui fonctionne à une fréquence de 2 GHz (Giga Hertz) génère 2 milliards
d’impulsions par seconde, soit une impulsion toutes les 5x10-10 secondes (0,5 nanoseconde).
La sortie d’un circuit asynchrone est évaluée (modifiée) dès l’instant où les signaux d’entrée sont
appliqués. Il peut néanmoins s’écouler un certain temps avant que la valeur de sortie soit stable.
Un circuit électronique synchrone possède toujours une entrée pour le signal d’horloge. Cette entrée
est souvent identifiée par la mention « clock » ou « clk » ou « ck ».
actif inactif
t1 t2 t3
Un symbole spécifique indique à quel moment la transition survient pour un composant donné :
(1) transition montante, (2) signal actif, (3) transition descendante, (4) signal inactif.
D Q D Q D Q D Q
Un circuit séquentiel est un circuit dans lequel la valeur des signaux de sortie à un instant donné ne
dépend pas uniquement des valeurs d’entrée du circuit à cet instant, mais également de l’historique
du circuit, c’est-à-dire de la séquence des transitions réalisées précédemment par ce circuit. Ce
résultat est obtenu par l’utilisation de boucles de rétroaction dans le circuit.
L’état d’un circuit séquentiel dépend donc de ses entrées, mais aussi de ses états précédents. Il
possède une mémoire du passé.
Remarque importante !
Nous allons voir qu’une bascule est un circuit séquentiel élémentaire qui possède deux états stables
(circuit bistable) et qui peut servir à réaliser une mémoire de 1 bit.
• bascule (flip-flop) dans le cas d’un circuit synchrone dont le changement d’état intervient lors
d’une transition du signal d’horloge (front montant ou descendant) ;
• verrou (latch) dans le cas
o d’un circuit synchrone dont le changement d’état est conditionné par le niveau du
signal d’horloge (haut ou bas),
o ou lorsque le circuit est asynchrone.
Nous parlerons quant à nous de bascule dans tous les cas de figure.
4.5.3 Bascule SR
4.5.3.1 Introduction
Une bascule SR (Set/Reset) est construite à partir de 2 portes NOR (ou 2 portes NAND) dont chaque
sortie est connectée à une entrée de l’autre porte.
Nous rappelons ci-dessous la table de vérité de la fonction NOR et nous donnons la représentation
du circuit d’une bascule SR et de ses deux états stables.
a b nor S
0
1 S
0
0
Q Q
0 0 1 0 1
0 1 0 1 0
1 0 0 0
Q 1
Q
R 0 R 0
1 1 0
• l’entrée S (Set) est utilisée pour mettre la bascule dans l’état 1 (Q=1) ;
• l’entrée R (Reset) est utilisée pour mettre la bascule dans l’état 0 (Q=0) ;
• la sortie 𝑄 donne l’état de la bascule (0 ou 1) ;
• la sortie 𝑄̅ donne le complément logique de la sortie 𝑄.
Si la bascule se trouve dans l’état initial 0 (𝑄=0). Le signal de sortie 𝑄 est connecté à la deuxième
entrée de la porte NOR supérieure. Les 2 entrées de la porte supérieure sont donc à 0. Il en résulte
une valeur de sortie 𝑄̅ égale à 1 (vu que 0 + 0 = 1). Le signal 𝑄̅ est quant à lui connecté à la porte
NOR inférieure, dont les entrées valent donc 0 et 1. Il en résulte une valeur de sortie 𝑄 qui est égale à
0 (vu que 0 + 1 = 0). Le circuit est donc dans un état consistant et stable. C’est la situation décrite
par le schéma de gauche : 𝑄 = 0.
Si nous effectuons le même raisonnement en supposant que la bascule est dans l’état initial Q=1,
nous obtenons également un état consistant et stable. C’est la situation décrite par le schéma de
droite. C’est le deuxième état stable de la bascule SR : 𝑄 = 1.
Nous aboutissons donc à une constatation étonnante : pour des valeurs d’entrée identiques (S=0 et
R=0), la bascule SR peut présenter deux valeurs de sortie différentes (soit 0, soit 1) ! Un tel
comportement est impossible à obtenir avec un circuit combinatoire. Il s’explique par la présence des
boucles dans le circuit.
La sortie 𝑄̅ de la porte NOR supérieure passe dans l’état 0 (0 + 1 = 0). Les deux entrées de la porte
inférieure ont maintenant la valeur 0, de sorte que la sortie 𝑄 passe dans l’état 1 (0 + 0 = 1). Les
deux entrées de la porte supérieure ont finalement la valeur 1, ce qui ne modifie pas la valeur de
sortie de 𝑄̅ qui reste à 0 (1 + 1 = 0). La bascule est à présent dans l’état 1.
Plus intéressant encore, la situation reste inchangée lorsque nous désactivons le signal d’entrée S
(S=0). Les deux entrées de la porte supérieure valent alors 0 et 1, la sortie 𝑄̅ reste à 0 (0 + 1 = 0).
Si la bascule est déjà dans l’état 1, appliquer un signal d’entrée S=1 ne modifie rien : 𝑄̅ = 1 + 1 = 0.
Reset
Le même raisonnement montre qu’en appliquant un signal d’entrée R=1 à la bascule, elle passe dans
l’état 0 quel que soit son état de départ.
En conclusion, activer l’entrée S (Set) fait passer la bascule dans l’état 1. Activer l’entrée R (Reset) fait
passer la bascule dans l’état 0.
La bascule SR présente alors un problème d’instabilité. En effet, tant que les deux entrées valent 1,
les deux sorties valent 0, ce qui n’est déjà pas très cohérent avec notre hypothèse que les sorties 𝑄
et 𝑄̅ sont complémentaires l’une de l’autre.
Mais la véritable question est : quel sera l’état final de la bascule lorsque les sorties repassent à
zéro ? Si une entrée reste à 1 plus longtemps que l’autre, c’est elle qui gagne. Si les deux entrées
repassent à 0 en même temps, le résultat est imprévisible.
Il peut être intéressant de contrôler à quel moment précis l’état du circuit doit être mis à jour. Pour
cela, nous allons rendre le circuit synchrone en utilisant un signal d’horloge.
Nous avons vu que si les signaux d’entrée S et R sont tous les deux à zéro, la bascule reste dans son
état stable courant. L’état de la bascule ne peut se modifier que si soit l’entrée S, soit l’entrée R passe
à 1. Comment pouvons-nous contrôler à quel moment un signal d’entrée actif (=1) est autorisé à
pénétrer dans le circuit ?
Nous avons expliqué à la section 4.4.4, qu’il existe 4 possibilités pour se synchroniser sur un signal
d’horloge : niveau haut, niveau bas, transition montante, transition descendante. Explorons-les.
Pour synchroniser l’entrée E sur le niveau bas de l’horloge, il suffit d’inverser la valeur du signal
d’horloge avec une porte NON (inverseur).
E E
S S
Figure 4-18 - Synchronisation sur les niveaux haut et bas d'une horloge
Le schéma ci-dessous présente la version synchrone sur niveau haut de la bascule SR. Comme nous
venons de l’expliquer, deux portes ET contrôle l’entrée des signaux S et R dans le circuit. Celle-ci est
autorisée uniquement lorsque le signal d’horloge vaut 1.
S
Q
Clock S Q
Clk
Q R Q
R
Nous expliquons dans la section suivante comment réaliser une véritable bascule au sens strict du
terme, c’est-à-dire un circuit synchrone dont l’état change lors d’une transition du signal d’horloge.
Nous pouvons conserver l’idée d’utiliser une porte ET pour contrôler le passage du signal d’entrée,
mais nous devons surtout concevoir un circuit capable de générer un « signal d’autorisation » qui
vaudra 1 uniquement pendant le court instant qui correspond à la transition du signal d’horloge de 0
vers 1 (ou de 1 vers 0).
Il est constitué d’une porte ET aux entrées de laquelle on connecte le signal d’horloge et sa valeur
inversée. Mathématiquement parlant, la sortie de la porte ET devrait toujours valoir 0, puisqu’elle
reçoit en entrée deux valeurs inverses l’une de l’autre (𝑎. 𝑎 = 0). En pratique ce n’est pas le cas, car
l’inverseur (comme toute autre porte logique) prend un certain temps pour changer d’état.
Clock
b
c
a AND b
a
Temps
Lorsque le signal d’horloge (a) passe dans l’inverseur, le signal d’horloge inversé (b) est légèrement
en retard sur le signal initial à cause du délai de propagation induit par la porte NOT.
Si on réalise un ET logique entre les signaux (a) et (b), on obtient donc une courte impulsion (a ET b)
dont la durée est égale au délai de propagation dans la porte NOT. Cette impulsion est à son tour
légèrement décalée à cause du délai induit par la porte ET, pour donner l’impulsion finale (c).
Au final, le circuit de la Figure 4-19 génère un train de brèves impulsions déclenchées par chaque
transition montante du signal d’horloge.
En suivant le même principe, nous pouvons construire un générateur d’impulsions déclenchées par
les transitions descendantes de l’horloge en remplaçant la porte ET par une porte NOR.
Le schéma ci-dessous présente la version synchrone sur transition montante de la bascule SR. Les
deux portes ET et le générateur d’impulsion contrôle l’entrée des signaux S et R dans le circuit.
S
Q
Clock S Q
Clk
Q
R
R Q
4.5.5 Bascule D
La bascule D (Delay ou Data) permet d’éviter le problème d’instabilité que la bascule SR présente
lorsque les 2 signaux d’entrée valent simultanément 1. Pour cela, on ne garde qu’un seul signal
d’entrée, appelé D. Ce signal alimente l’entrée supérieure de la bascule et son complément alimente
l’entrée inférieure. En procédant de la sorte, il devient impossible d’avoir 1 simultanément sur les
deux entrées de la bascule.
D
Q
Clock D Q
Q
Clk
Lorsque D vaut 1, on est dans la situation S=1/R=0 d’une bascule SR, la bascule passe dans l’état Q=1.
Lorsque D vaut 0, on est dans la situation S=0/R=1 d’une bascule SR, la bascule passe dans l’état Q=0.
Remarque : le schéma bloc à droite de la Figure 4-22 ne mentionne pas la sortie 𝑄. Cette sortie
complémentée n’est pas toujours disponible.
Tout comme nous l’avons déjà fait avec la bascule SR, nous pouvons créer une bascule D
synchronisée sur une transition du signal d’horloge en insérant le générateur d’impulsions adéquat
dans le circuit. Le schéma ci-dessous illustre le cas d’une synchronisation sur transition descendante
(le générateur d’impulsions utilise une porte NOR).
D
Clock Q
D Q
Q Clk
Dans la perspective qui est la nôtre, de chercher à construire un ordinateur, l’utilité principale d’une
bascule D est qu’elle permet de mémoriser la valeur appliquée à son entrée. Cette valeur mémorisée
est disponible en permanence à la sortie du circuit.
Pour réaliser un registre mémoire de n bits, il nous suffit de connecter en parallèle n bascules D,
comme le montre le schéma suivant :
D3 D2 D1 D0
Write
D Q D Q D Q D Q
Clk
Clk Clk Clk Clk
S3 S2 S1 S0
1. Activer l’entrée Write du circuit à 1 pour indiquer notre intention de modifier le registre.
2. L’écriture proprement dite s’effectue lors de la transition montante suivante du signal
d’horloge (entrée Clock).
Une fois qu’une valeur est stockée dans le registre, elle reste disponible en permanence sur les lignes
de sortie (Si) jusqu’à la modification suivante du registre.
4.5.7 Mémoires
Un registre permet de stocker une seule donnée d’une taille déterminée : 4, 8, 16, 32 ou 64 bits par
exemple.
Pour stocker davantage de données, nous pouvons bien sûr utiliser autant de registres que
nécessaire. Il est néanmoins indispensable d’ajouter une logique de contrôle d’accès à cette
collection de registres, de manière à pouvoir sélectionner correctement :
L’accès à un mot mémoire spécifique est basé sur une adresse, c’est-à-dire un nombre binaire qui
indique quel est le registre concerné. Avec une adresse de k bits, on peut adresser 2k mots mémoire
différents. C’est pour cette raison que le nombre de mots dans un circuit mémoire est toujours égal à
une puissance de 2.
Lors d’un accès en écriture (write), l’adresse de k bits doit permettre de sélectionner le registre dans
lequel écrire parmi les 2k mots mémoires. Nous sommes donc en présence d’un processus de
sélection « k vers 2k ». Un circuit décodeur ou démultiplexeur à k lignes de commande permet de
réaliser ce type d’opérations.
Lors d’un accès en lecture (read), l’adresse de k bits doit permettre de sélectionner celui des 2k mots
mémoires qui sera envoyé vers la sortie de la mémoire. Nous sommes donc en présence d’un
processus de sélection « 2k vers 1 ». Un circuit multiplexeur à k lignes de commande permet de
réaliser ce type d’opération.
Le schéma bloc ci-dessous illustre ce principe pour une mémoire de 4 mots dont les adresses ont une
taille de 2 bits.
Donnée
Adresse Write écrite
A0 E
A1 Demux « 2 vers 4 »
S3 S2 S1 S0
W D W D W D W D
R3 R2 R1... R0
S S S S
D3 D2 D1 D0
A0 Mux « 4 vers 1 »
A1 S
Read
Donnée lue
Le diagramme logique de la
Figure 4-26 présente une version détaillée possible du schéma bloc précédent pour une mémoire
contenant 4 mots de 4 bits chacun.
Bien que ce diagramme puisse paraître complexe à première vue, il n’en est rien, car le circuit est en
fait très régulier. Il peut être facilement étendu à une mémoire contenant plus de mots (répétition
des tranches horizontales) ou des mots plus longs (répétition des tranches verticales).
Chaque tranche horizontale du circuit correspond à un mot mémoire de 4 bits et adopte la même
structure que celle du registre de 4 bits présenté à la Figure 4-24.
Chaque ligne de donnée d’entrée Ei (E0 à E3) est câblée verticalement et est connectée à l’entrée D de
toutes les bascules stockant le bit i dans les différents mots mémoire.
La sélection d’un mot mémoire particulier est réalisée en décodant l’adresse. Dans notre exemple,
l’adresse est codée sur 2 bits ce qui permet d’adresser 4 mots mémoire distincts. Le décodage
nécessite par conséquent 4 portes ET à 2 entrées. Il s’agit d’un décodeur « 2 vers 4 ». Pour une
adresse donnée, une seule porte ET sera activée et elle activera la ligne de sélection horizontale
correspondant au mot mémoire choisi.
E3
E2
E1
E0
Donnée
écrite
Contrôle D Q D Q D Q D Q
d’écriture
Mot 0
Clk Clk Clk Clk
00
Ligne de
sélection D Q D Q D Q D Q
mot 0
Mot 1
Clk Clk Clk Clk
01
Ligne de
A1 sélection D Q D Q D Q D Q
mot 1
A0 Mot 2
Clk Clk Clk Clk
Adresse
10
Ligne de
sélection D Q D Q D Q D Q
mot 2
Mot 3
Clk Clk Clk Clk
11
Ligne de
sélection
Décodage adresse mot 3
Sélection mot
Write de sortie
Read S3
S2
Contrôle de S1
la sortie
S0
Donnée
lue
1. La donnée à écrire en mémoire est indiquée sur les entrées E0 à E3 (« Donnée écrite »).
2. L’adresse x du mot mémoire concerné est indiquée sur les entrées A0 à A1 (« Adresse »), ce
qui active la « Ligne de sélection mot x » de ce mot mémoire.
3. Lorsque le signal d’écriture « Write » est activé, une seule porte ET de « contrôle d’écriture »
est activée : celle qui correspond à la ligne de sélection du mot mémoire choisi.
4. La donnée d’entrée est écrite dans le mot mémoire sélectionné.
1. L’adresse x du mot mémoire demandé est indiquée sur les entrées A0 à A1, ce qui active la
« Ligne de sélection mot x » de ce mot mémoire.
2. La sortie de chaque bascule D est connectée à une porte ET (« Sélection mot de sortie ») dont
la deuxième entrée est raccordée à la ligne de sélection du mot mémoire dont cette bascule
fait partie.
3. Cela signifie donc qu’au sein d’une même tranche verticale (bit i d’un mot mémoire), il ne
peut y avoir à un instant donné qu’une seule bascule dont le signal est envoyé vers la porte
OU de sortie : celle qui correspond au mot mémoire sélectionné.
4. La sortie de chaque porte OU à 4 entrées est donc égale au bit correspondant du mot
mémoire sélectionné.
Remarque : le lecteur attentif réalisera que la combinaison du décodeur d’adresse avec une
tranche verticale composée de 4 portes ET et une porte OU n’est en fait rien d’autre qu’un
multiplexeur « 4 vers 1 »
5. Les portes ET dans la partie inférieure du diagramme (« Contrôle de la sortie ») permettent
de garantir que le contenu d’un mot mémoire n’est disponible sur les portes de sortie S0 à S3
(« Donnée lue ») du circuit mémoire que lorsqu’une opération de lecture (« Read ») est
demandée.
En théorie, il est possible de généraliser le diagramme logique présenté ci-dessus pour réaliser un
circuit mémoire comportant un grand nombre de mots de taille quelconque.
En pratique, il existe des contraintes physiques qui rendent la chose impossible. Ainsi, si la taille d’un
mot mémoire augmente, le diagramme se répète horizontalement : on ajoute autant de tranches
verticales qu’il y a de bits dans le mot mémoire. Les problèmes qui surviennent sont :
• le délai de propagation du signal sur les lignes horizontales augmente, ce qui ralentit le
circuit (= le temps d’écriture ou de lecture augmente) ;
• l’atténuation progressive du signal diminue la fiabilité du circuit ou nécessite une
régénération du signal (consommation + délai).
Si le nombre de mots mémoire augmente, on a bien sûr les mêmes problèmes de délai et
d’atténuation, mais cette fois-ci sur les lignes verticales. On a également les problèmes additionnels
suivants :
• le décodage de l’adresse nécessite des portes ET avec autant d’entrées qu’il y a de bits
d’adresse ;
• les portes OU de sortie doivent avoir autant d’entrées qu’il y a de mots mémoire !!!
Pour toutes ses raisons, la capacité d’un seul circuit mémoire sera généralement limitée afin de
pousser au maximum son optimisation. On construira des mémoires de grande capacité en
assemblant intelligemment des circuits de capacité plus faible.
L’idée générale est illustrée par le schéma de principe de la Figure 4-27 ci-dessous. Si on dispose de
circuits de 4 mots de 4 bits, on peut construire une mémoire de 16 mots de 8 bits en assemblant 8 de
ces circuits.
Mémoire 16x8
4x4 4x4
4x4 4x4
4x4 4x4
4x4 4x4
Pour adresser 16 mots mémoire, il faut une adresse de 4 bits (24=16). Les 2 bits de poids fort de
l’adresse servent à sélectionner une des 4 rangées de circuits. Les deux bits de poids faible de
l’adresse permettent de sélectionner 1 des 4 mots mémoire dans les circuits de la rangée
sélectionnée.
5 L’unité centrale de
5 traitement (CPU)
5.1 Introduction
Dans les chapitres précédents, nous avons expliqué comment il est possible de représenter différents
types d’informations en utilisant le système binaire. Nous avons également montré comment nous
pouvons construire divers circuits logiques et notamment les principaux composants de base d’un
ordinateur : unité arithmétique et logique (ALU), registre, mémoire.
Dans ce chapitre, nous allons expliquer comment utiliser ces connaissances pour construire une unité
centrale de traitement (CPU), c’est-à-dire un circuit électronique capable d’exécuter les instructions
d’un programme stocké en mémoire.
Il va de soi qu’un CPU réel est un circuit extrêmement complexe, qui est optimisé pour obtenir une
performance maximale. Nous nous contenterons de construire ici un CPU fictif très simplifié.
Notre processeur idéalisé est basé sur une architecture de type RISC inspirée de celle du processeur
MIPS. Nous avons choisi cette architecture pour sa relative simplicité. Elle nous permet d’illustrer la
mise en œuvre d’un CPU sans devoir entrer dans des considérations trop avancées.
Chaque instruction est représentée en mémoire par un code binaire, appelé code machine, qui
spécifie le type d’instruction à exécuter ainsi que ses arguments éventuels.
La liste complète des instructions comprises par un processeur ainsi que la spécification précise de
leur représentation interne (code machine) constituent l’architecture du jeu d’instructions ou ISA
(Instruction Set Architecture). C’est la bible de référence indispensable pour tout processeur.
Dans l’architecture MIPS, dont nous nous inspirons, les instructions sont représentées par un code
machine dont la taille est identique pour toutes les instructions, à savoir 32 bits, soit 4 octets.
Chaque instruction est alignée sur un mot mémoire de 32 bits. Par conséquent, l’adresse de début
d’une instruction en mémoire sera toujours un multiple de 4.
Suivant la nature de l’instruction à exécuter, le code machine MIPS utilise un des 3 formats suivants :
31 26 25 21 20 16 15 11 10 6 5 0
R OPCODE RS RT RD SHAMT FUNCT
I OPCODE RS RT CONSTANT/ADDRESS
J OPCODE ADDRESS
Figure 5-1 - Formats des instructions (code machine)
• OPCODE (Operation Code) – Il est présent dans toutes les instructions. L’opcode indique le
type d’instruction à exécuter (ex. opération arithmétique). Il permet également de
déterminer le format utilisé (R, I, J).
En simplifiant un peu les choses, nous pouvons dire que les différents formats sont utilisés dans les
cas suivants :
Dans les instructions MIPS, un registre est identifié par un numéro sur 5 bits : il y a donc au maximum
32 registres généraux adressables et utilisables par le programmeur
Chaque registre est destiné à un usage spécifique qui est fonction de son numéro. Chaque registre
porte également un nom qui reflète cet usage :
Outre ces 32 registres qui peuvent être référencés dans les instructions et dont le contenu peut être
modifié par le programme, notre CPU possède également quelques registres indispensables à son
fonctionnement interne, notamment :
Ainsi, l’instruction-machine qui réalise l’addition signée de deux registres est représentée
symboliquement dans le langage d’assemblage MIPS par :
Ainsi, si on souhaite additionner le contenu des registres $s0 et $s1 et placer le résultat dans le
registre $t0, on écrira simplement :
Un programme spécial, appelé assembleur, se charge de convertir cette instruction symbolique vers
le code machine correspondant, à savoir :
add add $s1, $s2, $s3 $s1 = $s2 + $s3 Addition : opérandes dans 3 registres
Arith- subtract sub $s1, $s2, $s3 $s1 = $s2 - $s3 Soustraction : opérandes dans 3 registres
métique
add immediate addi $s1, $s2, 20 $s1 = $s2 + 20 Utilisée pour ajouter une constante
load word lw $s1, 20($s2) $s1 = Memory[$s2 + 20] Mot : mémoire vers registre
store word sw $s1, 20($s2) Memory[$s2 + 20] = $s1 Mot : registre vers mémoire
load half lh $s1, 20($s2) $s1 = Memory[$s2 + 20] Demi-mot : mémoire vers registre
load half unsigned lhu $s1, 20($s2) $s1 = Memory[$s2 + 20] Demi-mot non signé : mém. vers registre
Transfert store half lh $s1, 20($s2) Memory[$s2 + 20] = $s1 Demi-mot : registre vers mémoire
de données
load byte lb $s1, 20($s2) $s1 = Memory[$s2 + 20] Octet : mémoire vers registre
load byte unsigned lbu $s1, 20($s2) $s1 = Memory[$s2 + 20] Octet non signé : mémoire vers registre
store byte sb $s1, 20($s2) Memory[$s2 + 20] = $s1 Octet : registre vers mémoire
load upper immed. lui $s1, 20 $s1 = 20 * 216 Constante dans 16 bits de poids forts
and and $s1, $s2, $s3 $s1 = $s2 & $s3 ET bit-à-bit : opérandes dans 3 registres
or or $s1, $s2, $s3 $s1 = $s2 | $s3 OU bit-à-bit : opérandes dans 3 registres
nor nor $s1, $s2, $s3 $s1 = ~($s2 | $s3) NOR bit-à-bit : opérandes dans 3 registres
Logique and immediate andi $s1, $s2, 20 $s1 = $s2 & 20 ET bit-à-bit : registre avec constante
or immediate ori $s1, $s2, 20 $s1 = $s2 | 20 OU bit-à-bit : registre avec constante
shift left logical sll $s1, $s2, 10 $s1 = $s2 << 10 Décalage à gauche valeur de la constante
shif right logical srl $s1, $s2, 10 $s1 = $s2 >> 10 Décalage à droite valeur de la constante
branch on equal beq $s1, $s2, 25 if ($s1 == $s2) go to Test d’égalité; branchement relatif à PC
PC + 4 + 100
branch on not equal bne $s1, $s2, 25 if ($s1 != $s2) go to Test d’inégalité; branchement relatif à PC
PC + 4 + 100
set on less than slt $s1, $s2, $s3 if ($s2 < $s3) $s1 = 1; Comparaison « inférieur à »
else $s1 = 0
Branchement
conditionnel set on less than sltu $s1, $s2, $s3 if ($s2 < $s3) $s1 = 1; Comparaison « inférieur à » non signée
unsigned else $s1 = 0
set on less than slti $s1, $s2, 20 if ($s2 < 20) $s1 = 1; Comparaison « inférieur à » constante
immediate else $s1 = 0
set on less than sltiu $s1, $s2, 20 if ($s2 < 20) $s1 = 1; Comparaison « inférieur à » constante
immediate unsigned else $s1 = 0 non signée
jump j 2500 go to 10000 Saut vers adresse de destination
Saut incon- jump register jr $ra go to $ra Saut vers adresse dans registre
ditionnel
jump and link jal 2500 $ra=PC+4; go to 10000 Appel d’un sous-programme
Pour une référence plus complète, on peut par exemple consulter la page Wikipedia suivante :
http://en.wikipedia.org/wiki/MIPS_instruction_set#Integer
Les langages de haut niveau permettent d’exprimer des expressions de calcul complexes avec une
seule instruction. Par exemple : r = m + n + o - p. Ce n’est pas le cas pour un langage d’assemblage
qui est composé d’instructions de base assez rudimentaires.
Un mode d’adressage est une méthode utilisée pour calculer la valeur d’une adresse en mémoire à
partir des opérandes d’une instruction (adresse, constante, numéro de registre) et/ou du contenu
d’un registre spécial tel que le compteur ordinal (PC).
Dans le cas d’une instruction de branchement, on distingue les principaux modes d’adressage
suivants :
• Adressage indirect – L’instruction indique dans quel registre se trouve l’adresse effective de
l’instruction suivante. [Format ‘R’]
Dans le cas de l’accès à une donnée, on distingue les principaux modes d’adressage suivants :
• Adressage immédiat – La donnée est présente directement dans l’instruction. Il n’y a donc
pas d’adressage à proprement parler. [Format ‘I’]
• Adressage implicite – L’opération utilise un registre qui est prédéfini par le processeur.
En effet, toutes les instructions ont une taille fixe de 32 bits, dont au moins 6 bits sont utilisés pour
l’opcode. Il reste donc au maximum 26 bits pour spécifier une adresse dans le format J et seulement
16 bits dans le format I. Or, une adresse complète nécessite 32 bits ! Comment faire pour s’en sortir ?
Nous avons indiqué à la section 5.2.1 qu’une instruction est toujours alignée sur un mot mémoire.
Par conséquent, son adresse est toujours un multiple de 4. Cela signifie que les deux derniers bits de
poids faible valent ‘0’. Il n’est donc pas utile de les préciser.
Ainsi, lorsqu’une instruction fait référence à l’adresse 400, elle désigne en réalité le mot mémoire
400, c’est-à-dire l’adresse 400*4=1600.
• les 4 bits de poids fort [31:28] du compteur ordinal (en réalité PC+4) ;
[voir remarque importante ci-dessous]
• les 26 bits d’adresse contenus dans l’instruction ;
• et 2 bits à 0.
L’instruction jump (j) utilise un mode d’adressage absolu, car l’adresse de l’instruction suivante est
précisée directement dans l’instruction. Ce type de branchement est illustré à la figure suivante.
Remarque importante : les 4 bits de poids fort du compteur ordinal étant utilisés sans modifications,
il faut obligatoirement que l’adresse finale à atteindre débute effectivement par ces 4 bits, ce qui
correspond à un bloc de 256MB en mémoire.
Le découpage en bloc de la mémoire est illustré à la figure suivante. Les registres ont une taille de 32
bits, ce qui limite la taille de la mémoire adressable à 4GB (=232 octets). Celle-ci est divisée en 16 (=24)
blocs de 256MB (=228 octets). Chaque bloc peut être identifié par un numéro de 4 bits. Il contient
226 = 64M mots mémoire de 4 octets.
L’instruction jump (j) ne permet pas d’atteindre une instruction située en dehors du bloc courant !
Dès lors, pour pouvoir atteindre de cette manière toutes les instructions du programme, il faut que :
1. le programme ait une taille inférieure à 228 octets (28 = 32-4), soit 256MB, ou encore 64M
instructions puisque chaque instruction occupe 4 octets ; et que
2. le programme soit positionné en mémoire de manière à ne pas être à cheval sur deux blocs
de 256MB.
Si ce n’est pas le cas, il faut utiliser une autre méthode de branchement (adressage relatif ou
indirect).
Dans le cas d’une instruction de branchement (format I), la situation est encore plus critique que
pour une instruction jump, car on dispose de seulement 16 bits pour spécifier une adresse !
Aussi, plutôt que de spécifier une adresse absolue, le champ « adresse / constante » d’une
instruction de branchement indiquera un décalage par rapport à l’instruction courante (PC), ou plus
exactement par rapport à l’instruction suivante légitime (PC+4).
L’instruction branch (beq, bne, …) utilise un mode d’adressage relatif, car l’adresse de
l’instruction suivante est exprimée par rapport à celle de l’instruction courante.
Puisque nous disposons de 16 bits pour exprimer le décalage, le branchement peut s’effectuer dans
une zone de ±215 = ±32.768 mots (instructions) par rapport à l’instruction courante ce qui est
largement suffisant dans la grande majorité des cas. L’adresse effective de l’instruction suivante
s’obtient donc comme suit :
• (PC+4) + décalage, où
• Décalage = constante de l’instruction (sur 16 bits) avec extension de signe à 30 bits + 00.
(Voir section 3.4.3.3 pour un rappel sur l’extension de signe.)
S’il n’est pas possible d’atteindre l’adresse voulue par une de ces deux méthodes (jump, branch),
alors la seule solution consiste à charger l’adresse sur 32 bits dans un registre et à utiliser le mode
d’adressage indirect : jump register (jr).
Exercice : comment est-il possible charger une adresse/constante sur 32 bits dans un registre,
sachant que les instructions de format I acceptent uniquement des constantes sur 16 bits ?
(Exemple adapté de « Computer Organization and Design », David A. Patterson & John L. Hennessy,
Ed. Morgan Kaufmann, Section 2.9, page 108.)
Nous supposons que les adresses de base des tableaux x[] et y[] se trouvent dans les registres $a0
et $a1.
Les registres temporaires $t0, $t1, $t2 et $t3 sont utilisés de la manière suivante :
int t0 = N;
int t1 = 0;
System.out.println(t1);
t0 = t0 – 1;
int t2 = 1;
System.out.println(t2);
t0 = t0 – 1;
Le programme en langage d’assemblage qui suit est une traduction assez directe du programme en
Java. Les différences principales sont :
• En Java, la boucle « while » exprime une condition de poursuite (t0 > 0). En assembleur
l’instruction « blez » exprime une condition d’arrêt : branchement si (t0 <= 0).
#
# fibonnacci() - Calcul des N premiers termes de la suite de Fibonnacci
# -> 0 1 1 2 3 5 8 13 21 34 55 89 144 ...
#
# Données constantes
.data
N: .word 12 # Nombre de termes à calculer
# Programme
.text
lw $t0, N # On initialise $t0 avec N (On suppose N >= 2)
add $t1, $zero, $zero # On initialise le premier terme
move $a0, $t1 # On charge la valeur entière à afficher dans $a0
jal PRINT # Appel du sous-programme d'impression -> print($a0)
addi $t0, $t0, -1 # On décrémente $t0
#
# Sous-programme d'impression d'un terme de la suite
#
PRINT: li $v0, 1 # Appel système : 1 = impression d'un entier
syscall # -> Affichage du terme suivant (valeur dans $a0)
li $a0, 10 # Charge le code ASCII du retour à la ligne '\n'
li $v0, 11 # Appel système : 11 = impression d'un caractère
syscall # -> Affichage du retour à la ligne (ascii dans $a0)
jr $ra # Retour du sous-programme d’impression (return)
Nous nous fixons comme objectif de construire un processeur capable d’exécuter un nombre limité
d’instructions.
1. Aller chercher en mémoire l’instruction dont l’adresse est stockée dans le compteur ordinal
(PC), puis ajouter 4 à la valeur du compteur ordinal (= adresse de l’instruction suivante).
2. Décoder l’instruction pour déterminer l’opération à réaliser, ainsi que le type et
l’emplacement des opérandes.
3. Obtenir les valeurs des opérandes (contenu d’un registre, contenu d’un mot mémoire,
constante ou adresse spécifiée directement dans l’instruction).
4. Exécuter l’instruction.
5. Sauvegarder le résultat éventuel (registre, mémoire), modifier la valeur du compteur ordinal
dans le cas d’un branchement.
6. Répéter la séquence au point 1
Construire un processeur consiste donc à assembler les composants qui permettent de mettre en
œuvre cette séquence d’opérations en tenant compte des différents types d’instructions.
Le circuit qui matérialise cette séquence, c’est-à-dire le cheminement des données à chaque étape
du cycle d’instruction, s’appelle un chemin de données (datapath).
Pour simplifier notre tâche, nous supposons que la mémoire contenant les instructions du
programme est distincte de celle qui contient les données.
Adresse
Add
P
Instruction somme
C
Mémoire
d’instructions
C’est-à-dire :
Si on ne tient pas compte des instructions de branchement qui peuvent introduire une rupture de
séquence, le circuit suivant permet d’aller chercher les instructions en mémoire et d’incrémenter le
compteur ordinal.
Add
4
P Adresse
C (read)
Instruction
Mémoire
d’instructions
/5 Read
register 1
Read /32
/5 Read data 1 ALUop
register 2
Registres zéro
/5
ALU
Write résultat
register Read /32
data 2
/32 Write
data
RegWrite
La banque de registres permet d’accéder en lecture à 2 registres dont les numéros (sur 5 bits) sont
indiqués en entrée : read register 1 et read register 2.
Les valeurs stockées dans les registres qui correspondent à ces numéros sont disponibles en
permanence sur les sorties read data 1 et read data 2 du circuit.
La banque de registres permet également d’écrire une donnée (write data) dans un troisième
registre dont le numéro (sur 5 bits) est spécifié en entrée (write register). La décision d’écrire ou pas
la donnée dans le registre est contrôlée par une ligne de contrôle (RegWrite).
L’ALU dispose de deux entrées sur 32 bits et fournit un résultat sur 32 bits. L’opération à réaliser est
déterminée par l’entrée de contrôle ALUop dont le nombre de bits dépend du nombre d’opérations
distinctes supportées par l’ALU. L’ALU fournit également un signal de sortie spécial « zéro » qui est
actif (= 1) lorsque le résultat est nul. Ce signal « zéro » servira plus tard dans le cadre de l’instruction
de branchement beq (branch if equal).
rs Read
register 1
Read ALUop
rt Read data 1
instruction register 2
zéro
Registres ALU
résultat
rd Write
register Read
data 2
value Write
data
RegWrite
Les numéros des 3 registres sont extraits directement des champs de l’instruction : rs et rt indiquent
les numéros des registres contenant les 2 opérandes, rd celui du registre où stocker le résultat.
L’ALU reçoit en entrée les données stockées dans les 2 registres d’opérandes. Sa sortie résultat est
connectée à l’entrée write data de la banque de registres, ce qui permet de sauvegarder la valeur du
résultat du calcul dans le registre indiqué par le champ rd de l’instruction (write register).
L’instruction load word (lw) permet de charger un mot mémoire dans un registre. Elle consiste à
réaliser l’opération suivante :
• R[rs] : c’est-à-dire la valeur (adresse) contenue dans le registre spécifié par le champ « rs »
de l’instruction ; et
• SignExtImm (Sign Extend Immediate) : qui correspond à la constante signée sur 16 bits
spécifiée dans l’instruction, à laquelle on applique une extension de signe de 16 à 32 bits.
Le contenu du mot mémoire débutant à cette adresse est transféré dans le registre spécifié par le
champ « rt » de l’instruction.
Exemple : lw $s0, 8($sp) → On ajoute 8 à l’adresse contenue dans le registre $sp (pointeur de
pile) et on charge le contenu du mot mémoire correspondant dans le registre $s0.
L’instruction store word (sw) permet de stocker le contenu d’un registre dans un mot mémoire. Elle
consiste à réaliser l’opération suivante :
Exemple : sw $s0, 0($sp) → On ajoute 0 à l’adresse contenue dans le registre $sp (pointeur de
pile) et on sauvegarde le contenu du registre $s0 dans le mot mémoire correspondant.
On constate que pour réaliser ces deux instructions, le chemin de données utilisé pour les
instructions de type R doit être adapté.
Pour choisir le bon type d’entrée en fonction de l’instruction exécutée, nous allons utiliser des
multiplexeurs. Nous avons vu à la section 4.2.2 qu’un multiplexeur est un circuit qui permet
d’aiguiller le signal d’une de ses N entrées vers sa sortie en fonction de la valeur d’un signal de
commande.
Dans notre cas, il y a seulement deux entrées possibles. Un signal de contrôle (0 ou 1) permettra de
choisir la bonne entrée.
rs Read
register 1
Read ALUop
rt Read data 1 MemWrite
instruction register 2
zéro Mémoire données
Registres ALU MemToReg
M ALUsrc résultat Adresse
Write
U Read data M
rd register Read M
X U
data 2 U
RegDst X
Write X Write data
data
MemRead
RegWrite
const Sign-
extend
Exemple : beq $0,$1,-25 → la constante spécifiée dans l’instruction vaut -25 (mots mémoire).
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Constante - - - - - - - - - - - - - - - - 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1
Extension Signe 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1
Décalage G de 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 0 0
Après extension de signe et décalage à gauche de 2 bits, la valeur obtenue est -100 (octets).
Pour compléter notre chemin de données, nous avons besoin d’un circuit qui réalise le décalage à
gauche de 2 bits (« shift left 2 »), ainsi que d’un additionneur pour calculer la nouvelle valeur de
compteur ordinal PC.
La sélection de la nouvelle valeur de PC est réalisée au moyen d’un multiplexeur. Ce multiplexeur est
contrôlé par la conjonction (ET) du signal zéro de l’ALU qui indique l’égalité des opérandes et du
signal de contrôle Branch qui indique une instruction de branchement beq.
M
Add U
X
4 Add
Shift
P Adresse rs Read left 2
C (read) register 1
Read ALUop Branch
Instruction MemWrite
rt Read data 1
Mémoire register 2
zéro Mémoire données
d’instructions Registres ALU MemToReg
M ALUsrc résultat Adresse
Write
U Read data M
rd register Read M
X U
data 2 U
RegDst X
Write X Write data
data
MemRead
RegWrite
const Sign-
extend
Le signal ALUop est quant à lui déduit de l’opcode et du code de fonction (bits 5 à 6).
Nous vous laissons le soin, à titre d’exercice, d’imaginer comment compléter le schéma de la Figure
5-10. Les éléments à prendre en considération sont les suivants :
Lors de la transition montante du signal d’horloge, l’instruction située à l’adresse contenue dans le
compteur ordinal (PC) est lue en mémoire. Cette instruction est alors exécutée.
L’écriture d’une donnée en mémoire, le stockage d’un résultat dans un registre et la mise à jour du
compteur ordinal (PC) s’effectuent sur la transition descendante du signal d’horloge. De cette
manière, on évite de modifier les données d’entrée pendant l’exécution de l’instruction.
Cette simplicité a un coût : le cycle d’horloge étant le même pour toutes les instructions, il doit donc
être supérieur ou égal au temps nécessaire à l’exécution de l’instruction la plus lente. Dans notre cas,
l’instruction la plus lente est l’instruction load (lw) qui nécessite 5 étapes : accès en lecture à la
mémoire d’instructions, accès en lecture aux registres, calcul de l’adresse mémoire effective avec
l’ALU, accès en lecture à la mémoire de données et écriture dans un registre.
Par contraste, une instruction de type R ne nécessite pas d’accès (lent) à la mémoire de données.
Enfin, une instruction de saut inconditionnel est la plus rapide puisqu’elle n’utilise ni les registres, ni
la mémoire de données, ni même l’ALU.
La pénalité due à un design où toutes les instructions s’exécutent en un seul cycle d’horloge est
acceptable pour des jeux d’instructions très simples. Ce n’est plus le cas si on introduit des
opérations de calcul en virgule flottante ou d’autres opérations complexes (ex. calcul vectoriel), car
l’écart de temps entre l’instruction la plus simple et la plus complexe est alors très significatif.
5.4 Pipelining
5.4.1 Améliorer les performances
Lorsque l’on conçoit le chemin de données d’un processeur, on essaie évidemment d’optimiser au
mieux chacun des composants qui interviennent dans la chaîne de traitement (banque de registres,
mémoire, ALU, etc.) afin d’obtenir une performance maximale.
Il n’est toutefois pas possible d’optimiser indéfiniment la performance de ces composants. Pour
améliorer encore les performances, il faut mettre en œuvre d’autres stratégies qui consistent à
réaliser plusieurs tâches en parallèle. Nous allons nous intéresser à 2 d’entre elles :
1. Le pipelining, qui consiste à exécuter les instructions successives d’un même programme
simultanément.
2. Le traitement vectoriel, qui consiste à exécuter la même instruction sur plusieurs données
simultanément.
Dans les deux cas, il s’agit d’introduire du parallélisme au niveau de l’exécution des instructions d’un
programme unique (instruction-level parallelism) et pas d’exécuter plusieurs programmes
simultanément.
Supposons par exemple que l’on souhaite faire du pain. Les tâches à réaliser sont les suivantes :
A. peser et mélanger les ingrédients (10 min),
B. pétrir la pâte (20 min),
C. laisser monter la pâte (30 min),
D. cuire le pain (45 min),
E. laisser refroidir le pain (10 min),
F. couper et emballer le pain (5 min).
Le temps total pour réaliser notre pain est donc de 120 minutes. Si on souhaite réaliser plusieurs
pains, il suffit de répéter les opérations précédentes. On obtiendra donc un pain toutes les 2 heures.
Pain 1 A B C D E F
Pain 2 A B C D E F
Pain 3 A B C D E F
Figure 5-12 - Travail en séquence
On suppose que le matériel utilisé ne permet pas de réaliser une tâche donnée pour plus d’un pain à
la fois. Il est néanmoins possible d’améliorer la situation en enchaînant les tâches de manière
autonome. Ainsi, dès que les ingrédients du pain 1 sont pesés et mélangés (tâche A), ils sont versés
dans le pétrin (tâche B). On peut alors commencer à peser et à mélanger les ingrédients du pain 2
sans attendre la fin du processus complet. Et ainsi de suite… L’enchaînement des tâches est décrit
par le tableau suivant :
Pain 1 A B C D E F
Pain 2 A B C D E F
Pain 3 A B C D E F
Pain 4 A B C D E F
Pain 5 A B C D E F
Pain 6 A B C D E F
Figure 5-13 - Travail à la chaîne / Pipelining
Si on fabrique un grand nombre de pains, on voit qu’une fois la chaîne de fabrication lancée, chaque
unité de travail (A à F) est occupée avec un pain différent.
A. Pesage et mélange : pain N+5
B. Pétrissage : pain N+4
C. Levage : pain N+3
D. Cuisson : pain N+2
E. Refroidissement : pain N+1
F. Découpe et emballage : pain N
1. Le temps nécessaire pour passer d’un poste au suivant est conditionné par la tâche la plus
lente, à savoir la cuisson, qui dure 45 minutes.
2. Le temps total nécessaire pour fabriquer un pain est donc de 6 x 45 = 270 minutes.
3. En revanche, dès que le premier pain est fabriqué, on obtient un nouveau pain toutes les 45
minutes. En situation de régime, on peut donc produire 32 pains par 24h au lieu de 12.
Un pipeline ne diminue donc pas le temps total nécessaire à la réalisation d’une seule tâche ! Par
contre, il augmente le débit, c’est-à-dire le nombre de tâches réalisées par unité de temps. Le cas
idéal est celui où une tâche complexe peut être décomposée en N sous tâches de même durée. Dans
ce cas, un pipeline à N niveaux n’allongera pas le temps d’exécution de la tâche de départ et il
apportera un débit N fois plus important par rapport à organisation monolithique.
On peut donc envisager de créer un pipeline à 5 étages : 1 étage pour chacune des 5 tâches.
Pour mettre en œuvre la technique du pipeline, il faut aussi faire en sorte que chaque étape de
l’exécution d’une instruction puisse être réalisée indépendamment de la précédente et de la
suivante. Naïvement, on peut proposer l’approche illustrée à la figure suivante.
A B C D
Add
4 Add
Shift
M left 2
P Adresse rs Read
U
C (read) register 1
X
Read
Instruction rt Read data 1
Mémoire register 2
zéro Mémoire données
d’instructions Registres ALU
résultat Adresse
M Write
M
rd U register Read M
Read data
U
X data 2 U X
Write X Write data
data
const Sign-
extend
Nous avons regroupé en 5 zones verticales clairement séparées les éléments qui correspondent à
chacune des phases du cycle d’exécution d’une instruction. Entre chaque phase, nous avons placé un
registre tampon (buffer). Son rôle est de stocker toutes les données générées au sein d’une phase
(ou provenant d’une phase précédente) qui seront utiles lors de la phase suivante (ou ultérieure).
Nous pouvons augmenter considérablement la vitesse de l’horloge, car le cycle d’horloge correspond
à présent au temps d’exécution de la phase la plus lente et non plus à celui d’une instruction
complète. Les données des registres tampons sont lues lors de la transition montante du signal
d’horloge et écrites lors de la transition descendante du signal d’horloge.
T1 :
T2 :
T3 :
T4 :
T5 :
Et ainsi de suite… Une fois que le pipeline est rempli, il constate qu’il y a 5 instructions exécutées
simultanément. Notre pipeline permet donc théoriquement de multiplier le débit par 5.
Rappelons qu’avec la technique du pipeline, le temps d’exécution d’une seule instruction n’est pas
plus court. Il peut même être plus long. Par contre, le pipeline permet de démarrer l’exécution de
l’instruction suivante sans attendre que l’exécution de l’instruction précédente soit terminée, ce qui
permet d’augmenter le débit, c’est-à-dire le nombre d’instructions exécutées par seconde.
On constate que la deuxième instruction utilise le résultat de la première ($t0). Or, le résultat de la
première instruction ne sera stocké dans le registre de résultat $t0 que dans l’étage 5 du pipeline. En
attendant que cette donnée soit disponible, l’exécution de l’instruction 2 ne peut pas se poursuivre.
Il faut donc retarder artificiellement l’exécution de l’instruction 2 pour laisser le temps à l’instruction
1 de sortir du pipeline.
Le processeur doit alors gérer les priorités d’accès et arrêter certaines instructions le temps qu’une
autre termine son exécution et libère la ressource convoitée.
Tous les événements décrits ci-dessus induisent des retards dans le pipeline et donc une perte de
performance.
Plus le pipeline est long et plus la probabilité qu’un tel événement survienne est grande. De même,
les retards engendrés sont proportionnels à la longueur du pipeline.
D’un autre côté, nous savons que le débit maximal théorique du processeur augmente avec le
nombre d’étages du pipeline.
Il faut donc trouver un juste compromis entre l’amélioration des performances et le coût des
pénalités. En pratique, dans de nombreux processeurs, la longueur du pipeline est comprise entre 10
et 15 étages. Le record est détenu par le processeur Intel Pentium 4 Prescott avec 31 étages !
5.4.5 Améliorations
Il existe heureusement de nombreuses techniques pour tenter de minimiser l’impact des problèmes
qui peuvent affecter le fonctionnement optimal d’un pipeline. Nous passons en revue les plus
courantes.
Reprenons le programme de la section précédente auquel nous avons ajouté une troisième
instruction :
Le choix de la séquence d’exécution optimale peut être réalisé directement par le processeur en
temps réel, ce qui complique fortement la logique de contrôle, soit au moment de la compilation du
programme si le compilateur connaît les spécificités propres à l’architecture du processeur.
Lorsque le résultat réel du branchement est connu, soit la prédiction est confirmée et tout va bien,
soit la prédiction se révèle fausse et il faut alors annuler toutes les instructions déjà exécutées et
reprendre l’exécution du programme au bon endroit.
Les algorithmes de prédiction peuvent être très simples à très complexes. Par exemple, on peut
supposer tout simplement que le branchement sera toujours réalisé. Cette technique naïve peut
s’avérer étonnamment efficace pour un grand nombre de boucles, puisque le branchement a
effectivement lieu à chaque itération sauf à la dernière. Les algorithmes plus sophistiqués
maintiennent quant à eux des statistiques qui permettent d’atteindre des taux de prédictions
réussies de plus de 95% !
Cette approche est particulièrement efficace pour traiter des algorithmes mathématiques qui
réalisent de nombreuses opérations sur des vecteurs ou des matrices : synthèse et traitement
d’images, calcul matriciel, traitement du signal, modélisation numérique, etc.
Un tel processeur dispose de plusieurs unités de calcul (nombres entiers, nombres en virgule
flottante, vecteurs) et de plusieurs chemins d’accès à la mémoire. Il peut alors exécuter
simultanément plusieurs instructions durant le même cycle d’horloge pour autant qu’elles utilisent
des unités fonctionnelles différentes. Par exemple, l’addition de 2 nombres entiers et la
multiplication de deux nombres en virgule flottante.
Chaque unité fonctionnelle peut à son tour utiliser un pipeline pour accélérer son fonctionnement
interne. Par exemple, l’addition de 2 nombres en virgule flottante peut se décomposer en 4 étapes :
Notons que, l’approche super scalaire reste pénalisée par les dépendances de données entre
instructions et par les instructions de branchement.
Dans sa version la plus simple, nous avons vu à la section 5.3.7 que la logique de contrôle du
processeur peut se résumer à implémenter une table de vérité. Il y a cependant deux éléments dont
nous n’avons pas encore tenu compte : les exceptions et les interruptions. Dans les deux cas, il s’agit
d’un événement, autre qu’une instruction de branchement ou de saut, qui va interrompre la
séquence normale d’exécution des instructions d’un programme.
Une exception est un événement imprévu dont la source est interne au processeur. Par exemple :
• Instruction inconnue.
• Erreur arithmétique (dépassement de capacité, division par zéro, …).
• Appel du programme au système d’exploitation.
• Problème matériel dans le processeur.
Une interruption est un événement imprévu dont la source est externe au processeur. Par exemple :
Pour déterminer l’action appropriée à prendre, le système d’exploitation doit connaître la nature de
l’exception ou de l’interruption.
La gestion des exceptions dans un processeur réel avec pipeline et/ou d’autres améliorations est bien
évidemment nettement plus complexe à mettre en œuvre.
Au moment où une exception survient, plusieurs instructions sont en cours d’exécution. Comment
déterminer l’instruction concernée ? Faut-il terminer l’exécution des instructions partiellement
exécutées ou les annuler complètement ? Faut-il vider le pipeline avant de transférer le contrôle au
système d’exploitation ?
La réponse à toutes ces questions détaillées sort du cadre de ce cours. Le lecteur intéressé consultera
avec profit l’ouvrage de référence « Computer Organization and Design », David A. Patterson & John
L. Hennessy, Ed. Morgan Kaufmann ».
66 Les mémoires
6.1 La hiérarchie des mémoires
Note : le contenu de ce chapitre est en partie basé sur celui de chapitre 7 du livre « Architecture et
technologie des ordinateurs, P. Zanella, Y. Ligier, E. Lazard, 5e édition, éditions Dunod ».
Après le CPU, la mémoire est un des éléments les plus fondamentaux de tout ordinateur. En effet,
nous avons besoin de mémoire pour stocker les instructions des programmes, ainsi que les données
sur lesquelles ils travaillent.
On pourrait rêver d’un ordinateur idéal dans lequel la quantité de mémoire serait illimitée et l’accès
à n’importe élément stocké dans cette mémoire infiniment rapide. Malheureusement, la réalité
technique est tout autre :
1. La capacité de stockage d’une mémoire, même si elle est très grande, est toujours limitée.
2. La vitesse d’accès à un élément en mémoire dépend de la technologie utilisée pour fabriquer
celle-ci.
Les limitations technologiques et les contraintes de coût font que ces deux aspects sont
généralement antagonistes :
• Les mémoires très rapides utilisent une technologie coûteuse et seront généralement de
faible capacité.
• Les mémoires de très grande capacité utiliseront quant à elles des technologies moins
coûteuses, mais plus lentes.
En pratique, on doit donc trouver un compromis entre le coût, la capacité et la performance. Suivant
la nature des données à stocker, l’usage qui en est fait et la fréquence de leur utilisation, on aura
alors recours à différents types de mémoires.
Ces différentes mémoires constituent une hiérarchie dans laquelle chaque niveau peut échanger des
données avec les niveaux adjacents. Du point de vue du programme, cela permet de créer l’illusion
d’une mémoire unique dont la capacité est quasiment illimitée.
• Registres – Petites mémoires extrêmement rapides situées au sein même du processeur. Les
registres servent à stocker les données directement manipulées par le processeur
(opérandes, résultats intermédiaires, compteur ordinal…)
Dans la hiérarchie des mémoires, la capacité de stockage augmente à chaque niveau, tandis que le
coût (par unité de donnée stockée) et la vitesse d’accès diminuent.
registres
s
nt
Ca
sa
mémoire cache
is
pa
cro
cit
éc
se
es
ro
mémoire centrale
vit
is
sa
&
nte
ix
Pr
NOTE : lorsque les préfixes kilo-, méga-, giga-, etc. sont utilisés pour indiquer la capacité
d’une mémoire centrale, ils font en fait référence à la puissance de 2 la plus proche. Ainsi une
mémoire de 1 ko contient 210 = 1024 octets, 1 Go = 230 = 1.073.741.824 octets, etc. Les
capacités des disques magnétiques expriment quant à elles des puissances de 10. Ainsi, un
disque de 1 Go contient 109 = 1.000.000.000 octets. On constate qu’il y a une différence de
7% entre les deux définitions du Go !
• Temps d’accès – C’est le temps total nécessaire pour réaliser une opération de lecture ou
d’écriture en mémoire.
• Cycle mémoire – C’est le temps minimal qui doit s’écouler entre deux accès successifs à la
mémoire.
• Débit – C’est la quantité d’informations lues ou écrites par seconde. Par exemple, un disque
connecté au moyen d’une interface SATA 3 peut atteindre un débit de transfert maximum
théorique de 6 Gbits/s.
• Volatilité – C’est la capacité (ou non) d’une mémoire à conserver de façon permanente les
informations qui y sont stockées en l’absence d’énergie électrique. Ainsi, les registres, la
mémoire cache et la mémoire centrale sont des mémoires volatiles. Si on coupe le courant,
le contenu est perdu.
Par contre, les disques optiques et les supports magnétiques sont non volatiles. Il en va de
même pour la mémoire flash utilisée dans les clés USB.
• Type d’accès – Désigne la manière dont il est possible d’accéder à une information
particulière :
o Accès direct – On accède directement à l’information voulue sur base de son adresse
(ex. mémoire centrale). C’est l’accès le plus rapide.
o Accès séquentiel – On doit lire toutes les données stockées en commençant à la
première, jusqu’à atteindre celle qui est désirée (ex. bande magnétique). Ce type
d’accès est le plus lent.
o Accès semi-séquentiel – Il s’agit d’une combinaison des deux accès précédents. Par
exemple, on accède directement à un gros bloc d’informations au sein duquel la
donnée recherchée nécessite une lecture séquentielle.
o Accès par le contenu (mémoire associative) – Une telle mémoire organise
l’information en associant chaque valeur mémorisée à une clé d’identification
unique. Retrouver une information consiste donc d’abord à rechercher la clé dans la
mémoire associative puis à renvoyer la valeur associée à cette clé.
Une dernière caractéristique à laquelle nous nous intéressons est la disponibilité des informations.
Cette caractéristique concerne les mémoires de masse pour lesquelles il n’est pas toujours possible
ni utile de pouvoir accéder en permanence à la totalité des informations sauvegardées. On
distingue ainsi :
• Le stockage en ligne (online) – Les informations sont directement disponibles. Elles sont
stockées sur un dispositif qui est accessible en permanence, soit parce qu’il est raccordé
directement à l’ordinateur (disque dur, disque SSD, voir même un lecteur de DVD pour
autant qu’on ne retire pas le disque du lecteur), soit parce qu’il est accessible à travers un
réseau.
• Le stockage quasi en ligne (near-line) – Les informations font partie d’une bibliothèque de
type juke-box contenant un grand nombre de supports de stockage (disques, bandes,
cassettes) et un ou plusieurs dispositifs de lecture. Lorsqu’une information est demandée, le
système de commande du juke-box détermine en premier lieu sur quel support elle se
trouve. Un robot se charge d’aller chercher le support sélectionné dans la bibliothèque et de
le placer dans un des lecteurs disponibles. Le temps d’accès initial peut varier de quelques
secondes à plusieurs minutes (par exemple si aucun lecteur n’est disponible).
• Le stockage hors ligne (offline) – Les supports contenant les informations sont stockés dans
une bibliothèque non automatisée. L’accès à cette bibliothèque peut être sécurisé
(confidentialité des données). Elle peut être localisée sur un autre site (plan catastrophe) et
aménagée pour assurer une longévité maximale des supports de données (température,
humidité, lumière).
La mémoire centrale offre un accès direct aux données stockées sur base d’une adresse. On parle
aussi d’accès aléatoire, d’où le nom de mémoire RAM (Random Access Memory) donné à ce type de
mémoire.
Il va de soi que toutes les mémoires offrent un accès en lecture, sans quoi elles ne seraient pas très
utiles. Par contre, suivant la technologie utilisée, les cellules mémoire peuvent être écrites de
nombreuses fois, une seule fois ou pas tout. On distingue les types de mémoires suivants :
• RWM (Read-Write Memory) – Mémoire dont les cellules peuvent être lues et écrites de
nombreuses fois. On parle également de mémoire vive et parfois, par abus de langage, de
mémoire RAM.
• ROM (Read-Only Memory) – Mémoire dont les cellules peuvent être lues, mais pas
modifiées. On parle également de mémoire morte. Le contenu d’une telle mémoire est
déterminé une fois pour toutes par le fabricant.
• PROM (Programmable ROM) – Mémoire morte programmable une seule fois par l’utilisateur
et de manière irréversible.
• EPROM (Erasable Programmable ROM) – Mémoire morte reprogrammable un certain
nombre de fois. L’effacement de la mémoire en vue de sa reprogrammation nécessite une
exposition aux rayons ultraviolets durant ±30 minutes. Toute la mémoire est effacée.
• EEPROM (Electrically Erasable Programmable ROM) – Même principe que pour l’EPROM, si
ce n’est que l’effacement est électrique, sélectif et rapide (1 minute).
Note : la mémoire FLASH utilisée dans les cartes mémoires des appareils photo numériques,
dans les clés USB et maintenant dans les disques SSD est une forme améliorée d’EEPROM qui
permet d’offrir de meilleures performances tant en lecture qu’en écriture.
On voit, notamment avec la mémoire FLASH, que la distinction entre mémoire vive et mémoire
morte s’est fortement estompée du point de vue de l’utilisateur. Pourtant, en interne les différences
restent significatives : une mémoire FLASH nécessite d’effacer les cellules mémoires avant de pouvoir
écrire à nouveau dedans. De plus, le nombre de cycles effacement/écriture d’une cellule mémoire
est limité. Une fois ce nombre atteint, les blocs mémoires contenant des cellules « mortes » sont
inutilisables, voire parfois la totalité de la puce mémoire. Ces contraintes nécessitent une gestion
assez sophistiquée de la mémoire flash pour garantir de bonnes performances et une longévité
maximale.
• SRAM (Static RAM) – Mémoire vive statique. Chaque cellule mémoire a une structure
similaire à celle d’une bascule D. Cette technologie est très rapide et consomme peu de
courant. Le qualificatif « statique » provient du fait que, tant que la mémoire est alimentée
en courant, son contenu est conservé indéfiniment sans devoir rien faire de particulier. Le
temps d’accès est de l’ordre de la nanoseconde ou moins. Elle est utilisée pour réaliser les
registres du processeur et la mémoire cache, là où une performance maximale est
nécessaire.
• DRAM (Dynamic RAM) – Mémoire vive dynamique. Chaque cellule mémoire utilise un
transistor et un petit condensateur. Le transistor charge ou décharge le condensateur suivant
qu’il faut stocker la valeur 1 ou 0. À la différence d’une SRAM, l’information stockée dans une
cellule de mémoire DRAM s’altère au fil du temps, car le condensateur se décharge
progressivement. Donc, même si le contenu de la mémoire n’est pas modifié, il est
nécessaire de rafraîchir régulièrement le contenu de chaque cellule afin de recharger le
condensateur des cellules qui contiennent la valeur 1. C’est ce qui explique le qualificatif
« dynamique » de ce type de mémoire. La nécessité de rafraîchir en permanence le contenu
de la mémoire rend la DRAM plus complexe et plus lente que la SRAM. Les temps d’accès
sont de l’ordre de quelques dizaines de nanosecondes. Par contre, comme chaque cellule
mémoire d’une DRAM n’utilise qu’un seul transistor et un condensateur, contre 6 transistors
ou plus dans le cas d’une SRAM, la densité d’intégration d’une DRAM est environ 4 fois plus
grande que celle d’une SRAM. Il est donc possible de fabriquer des puces de très grande
capacité à un coût nettement moins élevé.
Il existe plusieurs types de mémoires DRAM : FPM (Fast Page Mode), EDO (Extended Data
Output), SDRAM (Synchronous DRAM), DDR (Double Data Rate SDRAM), etc. Chaque
nouvelle génération de DRAM améliore principalement le débit des transferts de données
avec la mémoire.
Deux appellations coexistent pour identifier une barrette de DDR-SDRAM : DDR-xxx et PC-
xxx. L’appellation « DDR » (DDR, DDR2, DDR3, DDR4) fait référence au nombre de transferts
de données par seconde exprimé en MT/s. L’appellation « PC » (PC, PC2, PC3, PC4) fait quant
à elle référence au débit de données exprimé Mo/s et arrondi à la centaine la plus proche.
Dans une mémoire de type DDR (Double Data Rate), le nombre de transferts par seconde est
égal au double de la fréquence du bus système. Ainsi, si le bus système qui fonctionne à
1.600 MHz, on obtient une puce mémoire de type DDR4-3200. Et comme une puce DDR
traite 64 bits de données à chaque transfert, le débit en Mo/s est égal à : ((<fréquence en
MHz> x 2 x 64) / 8). Pour notre bus à 1.600 MHz, le débit est donc de (1.600 x 2 x 64) / 8 =
25.600 Mo/s. Une puce DDR4-3200 est donc équivalente à une puce PC4-25600 et permet
d’atteindre un débit maximum de 25,6 Go/s !
Les barrettes de mémoire les plus courantes sont les barrettes SIMM (Single Inline Memory Module)
et DIMM (Dual Inline Memory Module). Il s’agit de petits circuits imprimés comportant généralement
8 ou 16 puces mémoires. Les barrettes SIMM disposent de 72 contacts et peuvent transférer 32 bits
de données par cycle d’horloge. Les barrettes DIMM disposent de 120 contacts sur chaque face du
circuit (240 au total) et sont capables de transférer 64 bits de données par cycle d’horloge. Il existe
un troisième type de barrettes imaginé spécifiquement pour les ordinateurs portables appelé SO-
DIMM (Small Outline DIMM), qui est une barrette DIMM caractérisée par un encombrement réduit.
Une barrette DIMM typique contiendra par exemple 8 puces mémoires de 256 Mo chacune, offrant
ainsi une capacité totale de 2 Go.
Il existe différentes façons de répartir les données entre les puces. On peut par exemple choisir de
répartir les 8 bits d’un octet en stockant un bit sur chaque puce, voire même 1 bit par carte/barrette.
Les différents morceaux de l’adresse permettront d’identifier le numéro de carte, de puce et
d’emplacement au sein de chaque puce pour reconstituer l’octet mémoire demandé.
Le choix d’une configuration mémoire particulière a toujours pour objectif d’essayer de maximiser le
débit des transferts de données avec la mémoire. Certains contrôleurs mémoires imposeront le
transfert de données par bloc plutôt qu’octet par octet afin d’augmenter encore le débit.
Une dernière technique, appelée entrelacement, consiste à répartir les mots mémoire successifs sur
des barrettes mémoires différentes, ce qui permet de lancer un accès mémoire sur le mot suivant
sans attendre la fin du cycle précédent, puisqu’on s’adresse à un circuit différent.
Nous renvoyons le lecteur intéressé par davantage de détails sur ces différents aspects de
l’organisation physique de mémoires vers les ouvrages de référence.
Il existe une grande variété de supports de mémoire de masse différents. Nous nous contentons de
lister les plus courants, sans entrer dans les aspects techniques de leur fonctionnement. Le lecteur
intéressé trouvera facilement de nombreux sites sur Internet qui décrivent ces systèmes de stockage
en détail.
Les systèmes en ligne sont généralement basés sur des disques magnétiques qui permettent
d’accéder rapidement à de très grands volumes d’informations (base de données, serveurs de
fichiers, vidéothèque, etc.). Les systèmes d’archivage hors-ligne sont quant à eux plutôt basés sur des
bandes magnétiques ou des disques optiques qui sont beaucoup moins onéreux, mais aussi
nettement moins performants lorsqu’il s’agit d’accéder à une information stockée. Ces systèmes sont
indispensables pour pouvoir stocker les archives administratives d’un pays, ou encore sa production
littéraire, cinématographique, télévisuelle, etc. Dans notre société de l’information, cela représente
vite des volumes de plusieurs centaines, voire milliers de téraoctets!
Heureusement, les capacités et les densités de stockage d’informations ont progressé de manière
phénoménale depuis le début des années 1900. Pour s’en convaincre, il suffit de comparer, une carte
mémoire récente de type microSDXC avec le premier support de stockage de masse standardisé qui
était la carte perforée IBM de 80 colonnes :
• Une carte microSDXC récente permet de stocker 512Go de données dans une carte de
11x15mm, soit un volume total d’environ 158 mm3 et un poids de quelques grammes.
• Comme son nom l’indique, la carte perforée d’IBM permettait de stocker l’équivalent de 80
caractères alphanumériques sur une fiche cartonnée (165 gr/m2) de ±187x82mm.
• Pour stocker 512Go, il faudrait 6,8 milliards de cartes perforées, soit 3,4 millions de boîtes de
2000 cartes, ce qui représente un volume de stockage de l’ordre de 20.000 m3 et un poids
total approchant les 17.400 tonnes. En empilant toutes les cartes, on atteindrait une hauteur
de plus de 1200 km !
A. Plateaux
B. Bras
C. Tête de lecture/écriture
D. Cylindre
E. Piste
F. Secteur : bloc de taille fixe (par exemple 512
octets)
Source : « Basic disk displaying CHS ».
Sous licence Public domain via Wikimedia Commons -
http://commons.wikimedia.org/wiki/File:Basic_disk_displaying_CHS.svg#
mediaviewer/File:Basic_disk_displaying_CHS.svg
Un disque dur se compose de plusieurs plateaux (disques) en aluminium superposés. Ces plateaux
tournent à une vitesse de plusieurs milliers de tours par minute. Chaque plateau est recouvert d’un
film constitué d’une substance magnétisable. Le stockage d’informations binaires est réalisé en
modifiant l’orientation des particules magnétiques à la surface du disque de façon à représenter les
valeurs 0 et 1. La lecture des informations consiste quant à elle à détecter le sens d’orientation des
particules magnétiques. Ces opérations sont réalisées au moyen de têtes de lecture/écriture situées
entre les plateaux.
Un disque dur possède une structure bien définie qui sert à répartir et surtout à pouvoir retrouver
par la suite les informations stockées :
• Chaque plateau est divisé en cercles concentriques, appelés pistes (tracks). Tous les plateaux
possèdent le même nombre de pistes.
• L’ensemble des pistes correspondantes de tous les plateaux constitue un cylindre (cylinder).
• Chaque piste est à son tour divisée en un certain nombre de secteurs (sectors). Un secteur
est un bloc de données de taille fixe, par exemple 512 octets.
Initialement, un disque dur ne contient pas cette structure. C’est l’opération de formatage qui va
créer les subdivisions nécessaires sur les différents plateaux du disque.
Historiquement, toutes les pistes d’un disque dur contenaient le même nombre de secteurs. Cette
structure très régulière présente l’avantage de permettre un calcul aisé de la position d’une
information sur le disque. Par contre, comme la longueur des pistes diminue lorsqu’on se rapproche
du centre d’un plateau, si on stocke la même quantité d’informations sur chaque piste, cela signifie
que la densité linéaire des informations stockées est nettement plus grande au centre qu’à la
périphérie (cf. Figure 6-4 à gauche).
Les disques durs récents adoptent une organisation différente qui vise à garder une densité linéaire
constante et maximale sur toutes les pistes d’un plateau. Le nombre de secteurs par piste et la
quantité d’informations stockées augmentent au fur et à mesure que l’on s’écarte du centre du
disque. Cela complique un peu la tâche du contrôleur de disque, mais cela permet d’augmenter
significativement la capacité de stockage du disque. Cette technique porte le nom de Zoned Bit
Recording - ZBR (cf. Figure 6-4 à droite).
Pour lire ou écrire une information sur un disque dur, il faut donc déterminer le secteur auquel
accéder, mais aussi la piste et le plateau qui contiennent ce secteur. Chaque plateau possède sa
propre tête de lecture/écriture et est par conséquent accessible directement. Pour accéder à une
piste déterminée, la tête doit se déplacer pour atteindre la bonne position radiale sur le disque. Le
temps nécessaire pour effectuer ce déplacement porte le nom de temps de positionnement (seek
time). Toutes les têtes de lecture/écriture sont solidaires. On accède donc à la même piste
simultanément sur tous les plateaux, c’est-à-dire à un cylindre. Une fois la tête positionnée sur la
bonne piste, il faut attendre que le secteur souhaité passe sous la tête, ce qui demandera en
moyenne une demi-rotation du disque. Ce délai porte le nom de temps d’attente-rotation (rotational
latency). Il dépend de la vitesse de rotation du disque. Les données peuvent alors être lues ou écrites
à une vitesse maximale, ou encore taux de transfert maximum (burst rate), qui est fonction de la
vitesse de rotation du disque et de la densité de stockage des informations.
La vitesse maximale n’est d’application que pour le premier secteur lu et éventuellement pour un
certain nombre de secteurs additionnels, pour autant qu’ils se suivent sur une même piste ou au sein
d’un même cylindre, ce qui évite de devoir déplacer les têtes et/ou de subir un délai de rotation.
En pratique, le taux de transfert effectif sera nettement plus faible. Il va dépendre de la nature de
l’opération (lecture ou écriture) réalisée, de la taille des données à lire ou à écrire (beaucoup de
petits fichiers ou quelques gros), de la dispersion ou non des secteurs sur le disque (fragmentation),
de la performance du contrôleur de disque, de l’utilisation ou pas d’une mémoire cache, de la
performance de l’interface de connexion à l’ordinateur, etc.
Le taux de transfert moyen est une indication du débit moyen qu’un disque dur est capable de tenir
sur une longue période de temps et prenant en considération les différents temps d’accès évoqués
ci-dessus pour un mix supposé représentatif de fichiers et d’opérations.
Différentes technologies sont utilisées pour connecter un disque dur à un ordinateur. Les interfaces
les plus connues sont les suivantes :
• IDE (Integrated Drive Electronics) – Technologie apparue au milieu des années 80 dont l’idée,
novatrice à l’époque, était de décharger le processeur central de la gestion du disque, en
plaçant l’électronique de gestion sur une carte séparée intégrée au disque. L’adresse d’un
secteur de 512 octets est spécifiée sur 20 bits et est le reflet direct de l’organisation du
disque (plateau/piste/secteur). La capacité maximale d’un disque est limitée à 504 Mo. La
vitesse de transfert maximale est de 4 Mo/s.
• EIDE (Extended IDE) – Évolution de la norme IDE qui introduit la notion d’adressage de bloc
logique (LBA – Logical Block Addressing). Chaque secteur est identifié par un numéro sur 28
bits qui est indépendant de l’organisation physique du disque. C’est le contrôleur qui
effectue la conversion en fonction de l’organisation interne choisie. La capacité maximale
d’un disque est limitée à 128 Go. La vitesse de transfert maximale est de 16,67 Mo/s.
• ATA (AT Attachement), également appelé PATA (Parallel ATA) – Évolution de la vitesse de
transfert avec l’ordinateur. Les données sont transférées en parallèle entre le disque et le
CPU (larges connecteurs et rubans de câbles plats).
• ATAPI (ATA Packet interface) – La vitesse de transfert évolue à chaque génération (33 Mo/s,
66Mo/s, 100 Mo/s). La norme ATAPI-6 augmente également la taille de l’adresse d’un
secteur à 48 bits, autorisant de la sorte une capacité maximale de 128 pétaoctets !
• SATA (Serial ATA) – Le changement majeur est le transfert des données en série vers le
processeur, plutôt qu’en parallèle, ce qui permet d’accroître considérablement la vitesse de
transfert. La norme initiale a été conçue pour un débit maximal théorique de 1,5 Gbit/s. La
norme SATA II est passée à 3 Gbit/s, la norme SATA III à 6 Gbit/s (voire 16 Gbit/s avec la
révision 3.2). En pratique, le débit réel est de l’ordre de 150/300/600 Mo/s suivant la version
utilisée (1969 Mo/s en version 3.2).
• SCSI (Small Computer System Interface) – Interface permettant la connexion de maximum 7
périphériques (disque dur, scanner, DVD-ROM, …) à un bus de données partagé. Les
périphériques sont raccordés en série. Le succès de l’interface SCSI provient de ses vitesses
de transfert très élevées (640 Mo/s) qui en ont fait l’interface de choix pour les stations de
travail haut de gamme et pour les serveurs. De nos jours, l’interface SCSI a disparu des
ordinateurs grand public au profit du SATA ou de l’USB. Elle reste cependant très utilisée
dans le monde professionnel, avec des évolutions ultrarapides telles que le Fibre Channel qui
propose déjà des débits de 3,2 Go/s !
L’idée à la base du système est très simple : elle consiste à connecter plusieurs disques de faible
capacité (et donc peu coûteux) afin de créer un ensemble de grande capacité (appelé grappe) qui est
vu par l’ordinateur hôte comme un seul disque logique.
La signification initiale de l’acronyme RAID était « Redundant Array of Inexpensive Disks », c’est-à-
dire « groupement redondant de disques peu onéreux ». Actuellement, le « I » signifie
« Independent », c’est-à-dire « indépendants ». Ce changement de nom reflète l’évolution de la
finalité première du système RAID. En effet, avec l’évolution des technologies, l’objectif initial qui
était d’augmenter la capacité en réduisant le coût est devenu moins critique et a cédé la place aux
autres bénéfices de la technologie RAID, à savoir que l’utilisation de plusieurs disques indépendants
permet d’accroître la performance et/ou la tolérance aux pannes du système de stockage.
Le système RAID propose plusieurs méthodes standardisées pour organiser les données sur les
différents disques. On les appelle « niveaux RAID », bien qu’il n’y ait pas de hiérarchie entre eux. Les
niveaux les plus connus sont les niveaux 0 à 5.
contraire, il diminue la fiabilité globale du système puisqu’il suffit qu’un seul des N disques
tombe en panne pour perdre la totalité des données !
• RAID 2 – Ce niveau n’est plus utilisé. Il utilisait un code de Hamming pour protéger les
données contre la perte d’un disque. L’amélioration des performances en lecture/écriture
est obtenue en stockant chaque bit d’un octet (ou mot) de données sur 1 disque différent.
• RAID 3 – Le niveau 3 est une version simplifiée du niveau 2. Les données sont toujours
réparties bit par bit sur les différents disques, mais la protection des données est réalisée en
ajoutant simplement un bit de parité (et donc un disque). Si un disque tombe en panne et est
remplacé, tous les bits du disque de remplacement sont initialement à zéro. Si la vérification
de parité d’un mot est incorrecte, on sait que le bit correspondant sur le disque de
remplacement doit valoir 1.
• RAID 4 – Les données sont écrites bloc par bloc de manière circulaire sur les N disques (cf.
niveau 0) et non plus bit par bit comme pour les niveaux 2 et 3. La protection des données
est obtenue en calculant un bloc de parité pour chaque groupe de N blocs. Ce bloc de parité
est stocké sur le disque N+1. La limitation principale de ce système est que chaque fois qu’un
secteur est modifié, il faut également recalculer et écrire le bloc de parité correspondant. Le
disque de parité constitue vite un goulot d’étranglement si les données sont mises à jour
fréquemment.
• RAID 5 – Petite amélioration du niveau 4 qui consiste à répartir les blocs de parité sur les
différents disques de manière circulaire afin de limiter le goulot d’étranglement. En cas de
panne, la reconstruction du disque de remplacement est un processus complexe.
La gestion d’une grappe RAID peut être réalisée par voie logicielle ou matérielle.
Certains niveaux RAID peuvent être combinés entre eux. Par exemple, une grappe RAID 0 peut être
dupliquée en miroir, on parle alors de RAID 01 (ou RAID 0+1).
• DAS (Direct Attached Storage) – C’est l’architecture standard dans laquelle le système de
stockage est directement raccordé au serveur qui l’utilise.
• NAS (Networked Attached Storage) – Littéralement, il s’agit d’un système de stockage
connecté à un réseau, c’est-à-dire en fait un serveur disposant d’un système de stockage
local et raccordé à un réseau. Lorsqu’une application souhaite accéder aux données se
trouvant sur un NAS, elle envoie une requête au serveur distant en utilisant un protocole de
gestion de fichiers standard (NFS, AFP, …). Le serveur distant accède au système de stockage
et renvoie les données demandées à l’application cliente.
• SAN (Storage Area Network) – Un réseau de stockage n’est ni un système de stockage ni un
serveur de données. C’est une manière de partager des ressources de stockage de très
grande capacité entre plusieurs serveurs au moyen d’un réseau de communication
ultrarapide qui interconnecte les serveurs et les disques. Chaque serveur voit l’espace disque
qui lui est alloué comme s’il s’agissait d’un disque dur qui lui est directement attaché. L’accès
aux données s’effectue par bloc (secteur) comme pour un disque dur classique et non par
fichier.
Le processeur dispose de registres très rapides auxquels il a un accès direct, mais ceux-ci sont en
nombre limité et ne stockent qu’un seul mot mémoire. La mémoire centrale offre quant à elle une
grande capacité de stockage, mais elle utilise une technologie plus lente (DRAM) et présente un
temps d’accès moyen qui est très long par rapport à la durée du cycle d’instruction du processeur.
Une mémoire cache est un circuit mémoire très rapide (SRAM) et de capacité limitée. Il est placé
entre le processeur et la mémoire centrale. L'objectif est d’utiliser cette mémoire intermédiaire pour
y stocker les instructions et les données les plus utilisées. Cela permet de réduire au maximum le
nombre d’accès à la mémoire centrale et de limiter par conséquent l’impact de ces accès très lents.
Les processeurs modernes possèdent généralement plusieurs niveaux de mémoire cache. Par
exemple, l’architecture Sandy Bridge d’Intel possède 3 niveaux de cache :
• Cache de niveau 1 (L1) – Mémoire de 64 Ko, directement intégrée à chaque cœur. Pour être
tout à fait précis, cette mémoire cache est divisée en 2 morceaux : 32 Ko pour les
instructions et 32 Ko pour les données. Un accès demande 4 cycles d’horloge.
• Cache de niveau 2 (L2) – Mémoire de 256 Ko, directement intégrée à chaque cœur. Un accès
demande 11 cycles d’horloge. Mémoire cache commune (unified cache) pour les instructions
et les données.
• Cache de niveau 3 (L3) – Mémoire de 1 Mo à 20 Mo suivant les versions du processeur. Elle
se trouve sur la carte mère. Elle est partagée entre tous les cœurs et est aussi utilisée par le
processeur graphique.
Ainsi, lorsque le CPU a besoin d’une donnée, il interroge la mémoire cache de niveau 1. Si la donnée
n’est pas disponible, la mémoire cache de niveau 1 fait appel à celle de niveau 2. Celle de niveau 2 à
celle de niveau 3. Et finalement, la mémoire cache de niveau 3 va chercher l’information demandée
en mémoire centrale.
• Si un programme utilise une donnée (ou une instruction) située dans un emplacement
mémoire, il aura aussi tendance à utiliser peu après les données (ou instructions) situées à
proximité. On parle dans ce cas de localité spatiale.
• Si un programme utilise une donnée (ou une instruction), il aura tendance à réutiliser cette
donnée (ou instruction) dans un futur proche. On parle alors de localité temporelle.
La pertinence du principe de localité découle de la manière dont les programmes sont conçus et
exécutés, ainsi que de la façon dont les données sont stockées en mémoire.
Ainsi, si on considère un sous-programme qui trie un tableau de nombres, les données du tableau
sont généralement stockées dans une zone mémoire d’un seul tenant. Il en va de même pour les
instructions du sous-programme. Il en résulte une localité spatiale lors de l’exécution.
Par ailleurs, l’algorithme de tri va répéter plusieurs fois les mêmes séquences d’instructions (boucles)
et accéder de façon répétée aux éléments du tableau. Il en résulte une localité temporelle lors de
l’exécution.
Le lien entre le principe de localité temporelle et l’efficacité d’une mémoire cache est assez simple à
comprendre : lorsqu’une donnée (ou instruction) est utilisée, elle est placée dans la mémoire cache.
Si la même donnée (ou instruction) est utilisée à nouveau peu de temps après, elle est probablement
toujours présente dans la mémoire cache.
En ce qui concerne le principe de localité spatiale, l’idée est que si un mot mémoire doit être placé en
mémoire cache, alors on ne transfère pas uniquement ce mot isolé, mais bien tout un bloc auquel il
appartient. De cette façon, si le programme doit accéder au mot mémoire suivant ou précédent, il
sera probablement déjà dans la mémoire cache. Un tel bloc porte le nom de ligne de cache (cache
line). À titre d’exemple, l’architecture Sandy Bridge utilise des lignes de 64 octets.
Les principes exposés restent valables s’il y a plusieurs niveaux de cache, puisque chaque niveau
communique uniquement avec les deux niveaux directement adjacents.
Une mémoire cache est divisée en un certain nombre de lignes (blocs de données) contenant par
exemple 64 octets. Chaque ligne de la mémoire cache peut accueillir la copie d’un bloc de données
de même taille provenant de la mémoire centrale.
La figure suivante illustre ce principe pour une mémoire cache contenant 4 lignes.
0
1
2
3
0 4
1 5
2 6
3 7
8
9
10
11
Exemple : on souhaite accéder au contenu de l’adresse mémoire 46.323 et cette donnée n’est pas
encore présente dans la mémoire cache.
On va donc transférer les 64 octets compris entre les adresses 1011010011000000 (46.272) et
1011010011111111 (46.335) dans une ligne de la mémoire cache.
Puisque la mémoire cache peut contenir 16 lignes, on calcule « (adresse bloc) modulo 16 », ce qui
revient à garder les 4 derniers bits de l’adresse du bloc, c’est-à-dire 0011. Notre bloc de données sera
donc transféré dans la ligne 3 de la mémoire cache.
Adresse mémoire
1 0 1 1 0 1 0 0 1 1 1 1 0 0 1 1
Adresse bloc Index octet
1 0 1 1 0 1 0 0 1 1
Étiquette (tag) Index cache
Avec cette méthode de correspondance, tous les blocs de données dont l’adresse se termine par
‘0011’ utiliseront la ligne 3 de la mémoire cache. Comment savoir si celui qui nous intéresse est
effectivement présent dans la mémoire cache ou pas ?
Pour chacune des 16 lignes de données, la mémoire cache stocke les informations suivantes :
Lorsque le processeur souhaite accéder au contenu d’une adresse mémoire, le gestionnaire de cache
décompose l’adresse de 16 bits demandée comme suit :
• Bits 15 à 10 = tag
• Bits 9 à 6 = index de ligne
• Bits 5 à 0 = index de l’octet au sein de la ligne
1. Le bit de validité de la ligne de cache est égal à 0 (non) : la donnée n’est pas dans le cache.
2. Le bit de validité de la ligne de cache est égal à 1 (oui), mais le tag extrait de l’adresse est
différent de celui stocké dans la mémoire cache : la donnée n’est pas dans le cache.
3. Le bit de validité vaut 1 (oui) et les deux tags sont identiques : la donnée est présente dans le
cache. On utilise l’index de ligne et l’index d’octet pour obtenir la donnée voulue.
Lorsque l’adresse accédée n’est pas en mémoire cache, on parle d’un défaut de cache (cache miss).
Lorsqu’elle est présente en mémoire cache, on parle d’un succès de cache (cache hit).
La correspondance entre l’adresse d’un bloc en mémoire et l’index de la ligne de cache où il doit être
stocké étant fixe, le contenu d’une ligne de cache est remplacé dès l’instant où un bloc ayant le
même index de ligne doit être transféré vers le cache.
Dans une mémoire cache complètement associative (fully-associative cache), un bloc de données
peut être placé dans n’importe quelle ligne de la mémoire cache. La flexibilité est maximale.
0
1
2
3
4
5
6
7
8
9
10
11
Dans le cas du placement à correspondance directe, un seul des deux blocs de l’exemple ci-dessus
aurait pu être stocké en mémoire cache, car ils auraient dû occuper tous les deux la ligne 3 (0011).
Lorsque le processeur souhaite accéder au contenu d’une adresse mémoire, le gestionnaire de cache
décompose l’adresse de 16 bits demandée comme suit :
Il recherche ensuite s’il existe une ligne de cache valide dont le tag est égal à l’adresse du bloc
accédé. Il n’est pas concevable d’effectuer une recherche séquentielle, car le temps moyen
nécessaire pour trouver une entrée anéantirait le bénéfice d’utiliser une mémoire cache! Une
logique de recherche efficace est complexe à mettre en œuvre et n’est envisageable que pour des
mémoires caches de petite taille.
La figure suivante illustre ce principe pour une mémoire cache contenant 8 lignes organisées en 4
ensembles de 2 lignes chacun.
0
1
2
0
3
4
1
5
6
2
7
8
3
9
10
11
Ainsi, notre mémoire cache de 16 lignes pourrait être divisée en 4 ensembles de 4 lignes (22).
Chaque ensemble est indexé par un nombre de 2 bits qui est obtenu en effectuant le modulo 4 de
l’adresse du bloc, c’est-à-dire en prenant les 2 derniers bits de cette adresse.
Le tag servant à identifier le bloc stocké dans une ligne de cache est quant à lui constitué des 8 bits
restants de l’adresse du bloc.
Pour chacune des 16 lignes de données, la mémoire cache stocke les informations suivantes :
Lorsque le processeur souhaite accéder au contenu d’une adresse mémoire, le gestionnaire de cache
décompose l’adresse de 16 bits demandée comme suit :
• Bits 15 à 8 = tag
• Bits 7 à 6 = index de l’ensemble
• Bits 5 à 0 = index de l’octet au sein de la ligne
La recherche associative du tag est limitée aux 4 lignes de cache de l’ensemble sélectionné.
Dans une mémoire cache associative, on a le choix entre plusieurs lignes. On privilégiera en premier
lieu les lignes non utilisées. Si toutes les lignes sont utilisées, comment choisir celle qui doit être
remplacée ?
Cette dernière méthode nécessite de conserver une trace de l’utilisation des différentes lignes. Sa
complexité de mise œuvre augmente avec la taille des ensembles de lignes.
La technique la plus simple pour garantir la cohérence entre les différents niveaux de mémoire
consiste à répercuter immédiatement tout changement dans une mémoire cache vers le niveau de
mémoire supérieur. Cette technique porte le nom d’écriture immédiate (write-through). Elle est très
simple à mettre en œuvre, mais elle est peu efficace puisque chaque accès en écriture à la mémoire
cache nécessite aussi un accès en écriture vers la mémoire centrale. L’accès à la mémoire centrale
étant très lent, il ralentit le fonctionnement du processeur.
Une amélioration possible consiste à utiliser une file d’attente (write buffer) dans laquelle on stocke
les données à écrire en mémoire centrale. La mémoire cache continue à fonctionner à vitesse
maximale, tandis que les données sont écrites au fur et à mesure en mémoire centrale. Bien sûr, si le
rythme des opérations d’écriture dans le cache est trop important, il arrivera un moment où la file
d’attente est pleine. On retombe alors dans le cas précédent : la mémoire cache doit attendre que la
mémoire centrale ait terminé son travail.
Une alternative plus efficace est l’écriture différée (write-back). Dans un premier temps, les données
sont écrites uniquement dans la mémoire cache. Un bit de statut supplémentaire indique si une ligne
de la mémoire cache a été modifiée ou pas. Une ligne de cache modifiée est écrite en bloc dans la
mémoire de niveau supérieur lorsqu’elle doit être remplacée par les données d’un bloc différent. La
technique de l’écriture différée est nettement plus efficace, car elle ne nécessite pas un accès vers la
mémoire centrale pour chaque écriture.
• Une adresse physique qui représente une adresse précise en mémoire centrale, c’est-à-dire
une cellule déterminée dans un circuit mémoire.
• Une adresse virtuelle qui désigne quant à elle un emplacement dans un espace d’adressage
logique (virtuel).
Une table de correspondance permet de convertir à la volée une adresse virtuelle vers l’adresse
physique qui lui correspond.
La sécurité est améliorée du fait qu’un programme n’a pas d’accès direct au contenu de la mémoire
centrale. Il peut uniquement accéder à son espace d’adressage virtuel, c’est-à-dire aux portions de la
mémoire centrale qui lui ont été allouées par le système d’exploitation. Sauf autorisation explicite
(mémoire partagée), un programme ne peut pas accéder aux instructions et données d’un autre
programme.
1. Utiliser des adresses virtuelles dont la taille sera supérieure à celle des adresses physiques.
2. Gérer le fait que toutes les données de la mémoire virtuelle ne peuvent pas tenir
simultanément en mémoire centrale.
Nous avons vu comment la mémoire cache permet d’accéder rapidement à des portions de la
mémoire centrale en stockant localement une partie des instructions et des données d’un
programme. De la même manière, on peut imaginer que la mémoire centrale joue à son tour le rôle
de mémoire cache par rapport à la mémoire secondaire (disques magnétiques, SSD, …).
Le contenu complet de la mémoire virtuelle réside sur un périphérique de stockage de masse. Les
portions utilisées par le programme sont copiées en mémoire centrale à la demande, exactement
comme dans le cas d’une mémoire cache. Afin de limiter l’impact des temps d’accès au disque, les
données sont transférées par blocs relativement grands (de l’ordre de 16 Ko) appelés pages.
La figure suivante illustre le principe de fonctionnement d’une mémoire virtuelle de grande capacité.
Mémoire centrale
Page 0
Espace d’adressage Page 3
virtuel du programme Autre progr.
Page 6
Page 0
...
Page 1
Page 4
Page 2
Page 1
Page 3
Table de Autre progr.
Page 4
correspondance ...
Page 5
Page 6 Mémoire de masse
Page 7
...
Page 2
Page 5
Page 7
...
Bus système
Mémoire Mémoire
CPU
cache centrale
Bus d’entrées/sorties
Disque Imprimante
Périphériques d’entrées/sorties
Supposons que l’on souhaite lire les données stockées sur le secteur 1457 d’un disque dur. Cela
nécessite de réaliser une série de tâches : calculer la localisation du secteur sur le disque (n° de
plateau, n° de piste), déplacer la tête de lecture au bon endroit, lire les données du secteur,
transférer les données vers la mémoire centrale.
Lorsqu’une telle opération d’E/S doit être exécutée par le CPU, comment est-elle réalisée ? En
particulier, quel est le rôle joué par le CPU ? Quelle est la part de ces tâches qui est supervisée
directement par le CPU et celle qui est réalisée de manière autonome par le périphérique d’E/S ?
Le problème principal auquel nous devons faire face est l’extrême lenteur de fonctionnement des
périphériques d’E/S par rapport à la vitesse du CPU. Dans les ordinateurs de première génération, la
gestion des E/S était entièrement réalisée par le CPU et l’électronique de contrôle du périphérique
était réduite au strict minimum. Le CPU établissait une liaison directe avec le périphérique pour lui
transmettre toutes les instructions nécessaires à la réalisation d’un échange de données. Le CPU
devant travailler au rythme du périphérique, il passait un temps considérable à attendre !
Afin d’utiliser le CPU de manière plus rationnelle, l’idée est de permettre à chaque périphérique de
réaliser de manière autonome des tâches plus ou moins complexes et de mettre en place un système
de signalisation qui indique au CPU quand la tâche est terminée, ou quand le périphérique nécessite
une intervention du CPU.
Deux systèmes ont été envisagés pour permettre à un périphérique de signaler au CPU qu’il est prêt
à réaliser une opération d’E/S, ou lorsqu’il a terminé l’opération demandée par le CPU :
• Les drapeaux (flags) – Le périphérique signale qu’il est prêt en positionnant un drapeau (bit)
dans un registre de statut. Le processeur passe régulièrement en revue les drapeaux de tous
les périphériques afin de déterminer ceux qui sont disponibles ou dont il doit s’occuper.
Cette technique n’est pas très efficace, car le CPU doit surveiller en permanence tous les
drapeaux.
• Les interruptions (interrupts) – Le périphérique envoie un signal, appelé interruption, au CPU
pour signaler qu’il est prêt. Le processeur interrompt l’exécution du programme en cours et
exécute un programme de gestion spécial qui réalise l’opération d’E/S (cf. section 5.7
« Exceptions et interruptions »). L’approche par interruptions est la plus répandue.
L’importance des tâches qu’un périphérique d’E/S est à même de réaliser de manière autonome sans
intervention du CPU dépend de :
Que se passe-t-il lorsque plusieurs périphériques d’E/S envoient simultanément des interruptions au
CPU ? Ou bien lorsqu’une interruption est reçue alors que le traitement de la précédente n’est pas
encore terminé ?
Les ordinateurs modernes mettent en place un système d’interruptions hiérarchisées. Dans un tel
système, une priorité est attribuée à chaque interruption.
• Des interruptions de même priorité sont traitées dans leur ordre d’arrivée.
• Le programme de traitement d’une interruption peut lui-même être interrompu par une
interruption de priorité supérieure.
• Lorsque le traitement d’une interruption est terminé, le contrôle du CPU est attribué au
programme en attente qui a la priorité la plus élevée.
On obtient ainsi une cascade d’appels imbriqués des sous-programmes de gestion d’interruptions.
Les contextes d’exécution étant généralement sauvegardés sur une pile.
Les systèmes de gestion d’interruptions peuvent être très sophistiqués. Parfois, ils jouent même un
rôle critique. C’est par exemple le cas dans les applications en temps réel (acquisition de mesures,
contrôle de processus industriels…) où il est indispensable de réagir à une interruption dans un laps
de temps maximum imposé.
Il existe une très grande variété de périphériques d’entrées/sorties possédant des caractéristiques
très différentes : type d’accès, vitesse de transfert, taille des données, connectique, commandes de
contrôle, etc. Afin de faire face à une telle diversité, l’ordinateur impose une interface standardisée
pour communiquer avec les périphériques d’E/S.
Les contrôleurs modernes sont de plus en plus sophistiqués. Ce sont des processeurs à part entière
qui ajoutent de nombreuses fonctionnalités supplémentaires : gestion d’une mémoire cache,
détection d’erreurs, cryptage et décryptage à la volée, etc.
Bus système
CPU Mémoire
Bus d’entrées/sorties
Contrôleur Contrôleur
périphériques périphériques DMA
Contrôleur
périphériques
Le CPU démarre l’opération d’E/S en fournissant au contrôleur DMA toutes les informations
nécessaires : périphérique concerné, type d’opération (lecture/écriture), quantité de données à
transférer, adresse de début en mémoire centrale. Le DMA réalise le transfert de données de
manière autonome et signale la fin de l’opération en envoyant une interruption au CPU.
Comme il n’est généralement pas possible d’effectuer deux accès simultanés à la mémoire centrale,
il arrive que le CPU et le DMA entrent en conflit. Il faut donc prévoir un mécanisme de gestion des
priorités d’accès à la mémoire.
Généralement, le DMA est prioritaire sur le CPU. Lorsqu’il doit effectuer un transfert de données, le
DMA obtient un accès exclusif à la mémoire pendant toute la durée de ce transfert. Le CPU est alors
mis en attente. On appelle cette technique le vol de cycles, car le DMA vole des cycles d’accès à la
mémoire au CPU. Le fonctionnement d’un périphérique d’E/S étant nettement plus lent que celui du
CPU, le DMA n’est heureusement pas capable de monopoliser l’accès à la mémoire en permanence.
Pour limiter les conflits d’accès et garantir au CPU un accès performant à la mémoire, on peut prévoir
un bus d’E/S séparé du bus système et utiliser une mémoire à double accès (cf. Figure 7-2).
Contrairement au DMA qui réalise uniquement des transferts de données, un canal est capable
d’exécuter de véritables programmes d’entrées/sorties et d’enchaîner plusieurs opérations
successives. Le CPU intervient uniquement pour démarrer le programme de canal adéquat et lui
fournir les paramètres nécessaires.
Un canal d’E/S peut être dédié à un seul périphérique (canal sélecteur) ou partagé entre plusieurs
périphériques à faible débit (canal multiplexé).
Un canal d’E/S peut être connecté directement au(x) périphérique(s) qu’il contrôle, ou y accéder par
l’intermédiaire d’un bus d’E/S.
CPU Mémoire
Bus système
Bus d’entrées/sorties
Contrôleur
périphériques
Contrôleur Contrôleur
périphériques périphériques
Un bus interne interconnecte différents composants au sein d’un ordinateur. Un bus externe permet
de connecter des périphériques à un ordinateur, ou de connecter plusieurs ordinateurs entre eux.
• Le bus système qui connecte le CPU à la mémoire centrale à très grande vitesse.
• Le bus d’entrées/sorties, également appelé bus d’extension, qui permet de connecter des
périphériques d’E/S ou des cartes d’extension.
La transmission des informations sur un bus peut être synchrone (cadencée par une horloge) ou
asynchrone (transfert initié à la demande du CPU ou d’un contrôleur d’E/S).
Les données peuvent être transmises sur le bus un seul bit à la fois (bus série) ou plusieurs bits à la
fois (bus parallèle).
Enfin, des bus de différents types peuvent être interconnectés en utilisant un élément intermédiaire
appelé pont.
• PCI (Peripheral Component Interconnect) – Bus parallèle de 32 (ou 64) bits partagé entre tous
les périphériques connectés. Il permet un débit théorique de 133 Mo/s (ou 266 Mo/s).
• PCI Express (PCIe) – Bus série qui transporte les données par paquets. Il offre un débit qui
varie entre 250 Mo/s et 64 Go/s suivant les versions. Au sens strict du terme, PCI Express
n’est pas un bus, il s’agit en fait d’un ensemble de liaisons (links) point à point, qui
connectent les périphériques à un nœud central (root complex). La transmission des
informations s’effectue en série (1 bit à la fois) sur des lignes (lines) bidirectionnelles qui
offrent un débit de 250 Mo/s à 2 Go/s dans chaque sens. En fonction du débit nécessaire au
périphérique, une liaison peut utiliser 1, 2, 4, 16 ou 32 lignes.
Dans la figure ci-après, on voit que la norme PCI Express est conçue pour pouvoir fonctionner avec
l’ancienne norme PCI. Un pont (PCI Bridge) réalise l’adaptation entre le bus PCI et le monde PCIe.
CPU Mémoire
Périphérique
Switch PCI Bridge
Périphérique PCIe
PCIe
Bus PCI
Périphérique
Périphérique PCIe
PCIe
Périphérique Périphérique
PCI PCI
Un périphérique externe (disque dur, souris, webcam) est raccordé à l’ordinateur au moyen d’un bus
spécifique, appelé bus externe. Le contrôleur de ce bus externe est un circuit présent sur la carte
mère de l’ordinateur, ou c’est une fonctionnalité ajoutée au moyen d’une carte d’extension.
Les principaux bus externes utilisés actuellement sont : eSATA, SCSI, FireWire (IEEE 1394), USB,
ThunderBolt, Fibre Channel, InfiniBand.
Nous renvoyons le lecteur aux ouvrages de référence pour obtenir des informations détaillées sur ces
différentes technologies.
L’objectif de cette section n’est pas d’expliquer en détail le fonctionnement des différents types de
réseaux informatiques, mais uniquement d’introduire brièvement quelques notions de base, ainsi
que les éléments de vocabulaire correspondants.
• Réseau personnel – PAN (Personal Area Network) : réseau dont les nœuds se trouvent dans
un voisinage immédiat de quelques mètres (clavier, souris, scanner, imprimante…). Un PAN
interconnecte les équipements au moyen d’un bus externe (ex. USB) ou d’une technologie
sans fil (BlueTooth, WiFi, …). Dans ce dernier cas, on parle de WPAN (Wireless PAN).
• Réseau local – LAN (Local Area Network) : réseau dont les nœuds se trouvent dans le même
bâtiment ou dans des bâtiments voisins. La distance couverte est de quelques dizaines ou
centaines de mètres. Un réseau local utilise généralement la technologie Ethernet (fils de
cuivre ou câble coaxial), ou une transmission sans fil de type WiFi. Dans ce dernier cas, on
parle de WLAN (Wireless LAN).
• Réseau métropolitain – MAN (Metropolitan Area Network) : réseau dont les nœuds se
trouvent dans une même métropole. La distance couverte est de quelques kilomètres à une
dizaine de kilomètres au plus. Le support de transmission est généralement la fibre optique.
• Réseau étendu – WAN (Wide Area Network) : réseau dont les nœuds sont
géographiquement très éloignés, de quelques centaines à quelques milliers de kilomètres.
Internet est un WAN à l’échelle de la planète.
Si on désire envoyer le même message à de nombreux utilisateurs, on peut bien sûr l’envoyer
individuellement à chaque utilisateur, mais il est plus efficace de l’envoyer simultanément :
• à tous les utilisateurs, on parle alors de diffusion (broadcast), c’est le même principe que
celui utilisé pour la radiodiffusion ;
• à un groupe limité d’utilisateurs, on parle alors de multidiffusion (multicast), c’est le mode
de transmission généralement utilisé pour la vidéoconférence entre les membres d'un même
groupe.
• Réseau en arbre – Les nœuds du réseau sont répartis sur plusieurs niveaux
hiérarchiques. Les nœuds d’un même niveau n’ont pas de connexion
directe. Ils communiquent entre eux par l’intermédiaire d’un nœud de
niveau supérieur.
Un réseau réel sera généralement constitué d’une combinaison de ces différentes topologies.
88 Le système d’exploitation
8.1 Introduction
Jusqu’à présent, nous nous sommes beaucoup intéressés à l’ordinateur du point de vue de son
architecture matérielle :
En pratique, ce qui fait l’utilité d’un ordinateur, ce sont surtout les logiciels qu’il est capable
d’exécuter. Ce sont eux qui tirent parti de la véritable puissance de l’outil informatique.
Le système d’exploitation (Operating System – OS) est le maillon indispensable situé entre les
couches matérielles (partie système) et les couches logicielles (partie utilisateur) de l’ordinateur.
Le rôle principal du système d’exploitation est, comme son nom l’indique, de permettre l’exploitation
de l’ordinateur. A ce titre, le système d’exploitation :
• Assure la gestion des ressources matérielles de l’ordinateur, telles que le CPU ou la mémoire.
• Organise l’exécution des programmes.
• Prend en charge la gestion des périphériques d’entrées/sorties.
• Assure la gestion des exceptions et des interruptions.
• Organise le stockage des informations sur les dispositifs de stockage (systèmes de fichiers).
• Fournit à l’utilisateur un ensemble d’instructions supplémentaires et de sous-programmes
(system calls) qui ajoutent des fonctionnalités de plus haut-niveau et qui lui facilitent la vie.
• Facilite l’interaction de l’utilisateur avec la machine au moyen d’une interface de commande
par ligne et/ou d’une interface graphique.
• Contient un ensemble de logiciels utilitaires qui aident à la maintenance du système
(formatage des disques), au développement d’applications (assembleur, compilateur,
débogueurs, …), à la communication avec d’autres ordinateurs (ftp, telnet, …), etc.
Avec le temps, les tâches assurées par le système d’exploitation se sont diversifiées. Un système
d’exploitation moderne propose un ensemble de programmes toujours plus vaste dont la finalité
s’écarte de plus en plus de son rôle initial. Ainsi, les systèmes d’exploitation Linux, MacOS ou
Windows sont constitués de dizaines de logiciels utilitaires et applicatifs qui offrent des
fonctionnalités qui vont bien au-delà de la simple exploitation des ressources matérielles de
l’ordinateur.
8.2 Le noyau
Au cœur de tout système d’exploitation se trouve le noyau (kernel). Le noyau est la portion du
système d’exploitation qui se charge de l’interface entre le logiciel et le matériel au niveau le plus
bas.
Le noyau s’occupe de :
Ces tâches sont à la base du fonctionnement même de l’ordinateur. Elles sont exécutées en
permanence et nécessitent donc une efficacité maximale.
Pour cette raison, le noyau est la seule portion du système d’exploitation qui doit résider en
permanence en mémoire centrale. Le noyau est généralement codé directement en assembleur pour
garantir que son exécution soit la plus rapide possible.
Un système multitâche est envisageable même si l’ordinateur ne dispose que d’un seul processeur.
En fait, que l’ordinateur contienne un processeur ou plusieurs ne change pas grand-chose. Dans la
plupart des cas, le nombre de tâches à exécuter sera de toute façon supérieur aux nombres de
processeurs disponibles. La simultanéité d’exécution apparente de toutes ces tâches est obtenue par
le fait que chaque processeur alloue une partie de son temps en alternance aux différents
programmes qu’il doit exécuter. L’alternance rapide entre les tâches donne l’impression d’une
exécution simultanée.
Différentes techniques ont été utilisées pour permettre l’exécution simultanée de plusieurs
programmes par un ordinateur :
Un second processus appelé allocateur (dispatcher) est chargé de répartir le temps du CPU entre les
différents processus de la file d’attente.
Lorsque le processeur passe d’un processus à un autre, on dit qu’il effectue un changement de
contexte (context switching). Il faut sauvegarder l’état d’exécution du processus interrompu, c’est-à-
dire son contexte, afin de pouvoir poursuivre son exécution ultérieurement. Le contexte d’un
processus contient notamment les valeurs stockées dans les registres du processeur.
2. La mémoire est divisée en partitions de tailles variables. La taille d’une partition correspond
exactement à la quantité de mémoire demandée par un programme. Les inconvénients de ce
système sont :
• La taille d’une partition est fixée une fois pour toutes lors du chargement du
programme. Si celui-ci ne connaît pas exactement à l’avance la quantité de données
qu’il va devoir traiter, il aura tendance à réserver plus de mémoire que nécessaire, ce
qui entraîne à nouveau du gaspillage.
• Une partition occupe une zone mémoire d’un seul tenant. Au fur et à mesure de
l’exécution des programmes, la mémoire libre va avoir tendance à se fragmenter et il
devient de plus en plus difficile de trouver la place nécessaire pour stocker une
partition. Il faut régulièrement compacter la mémoire, c’est-à-dire déplacer les
partitions en mémoire pour regrouper les zones utilisées et recréer de grandes zones
libres continues.
3. Pour s’affranchir de la contrainte de devoir allouer une zone mémoire d’un seul tenant à un
programme, l’étape suivante consiste à diviser un programme en segments, c’est-à-dire en
portions plus ou moins petites qui contiennent seulement une partie des instructions ou des
données. Fondamentalement, chaque segment est géré comme une partition de taille
variable. La taille plus réduite des segments fait que la fragmentation de la mémoire est
moins problématique et diminue la fréquence des réallocations. De plus, un programme peut
allouer/libérer dynamiquement des segments durant son exécution, ce qui lui permet
d’utiliser la mémoire plus efficacement pour stocker ses données. Le système d’exploitation
maintient la liste des segments utilisés par chaque programme.
Dans les 3 méthodes exposées ci-dessus, une question se pose : comment empêcher un programme
d’aller lire, ou pire, d’aller modifier la zone mémoire allouée à un autre programme ou celle utilisée
par le système d’exploitation ?
• Chaque programme utilise le mode d’adressage relatif. Il n’utilise pas des adresses
mémoires absolues, mais calcule toute adresse effective relativement au contenu d’un
registre de base.
• Le registre de base contient l’adresse de début de la partition (ou du segment) en mémoire.
• Un deuxième registre, appelé registre de borne, contient quant à lui l’adresse supérieure de
la partition (ou du segment).
• Seul le système d’exploitation est autorisé à modifier le contenu des registres de base et de
borne lors de l’activation d’un processus.
• Lorsqu’un programme s’exécute, un dispositif du processeur vérifie que toute adresse
référencée par le programme appartient bien à la zone mémoire allouée au processus. Si ce
n’est pas le cas, une exception est générée afin d’alerter le système d’exploitation.
• La séparation complète entre les adresses utilisées par un programme (adresses virtuelles
dans un espace d’adressage fictif propre au programme) et les adresses physiques qui seront
utilisées une fois qu’il sera chargé en mémoire centrale.
• Le découpage de la mémoire centrale en pages (blocs de mémoire) et son utilisation comme
mémoire cache d’une mémoire virtuelle stockée en mémoire secondaire (disque dur, SSD).
La différence provient du fait qu’un thread n’est pas un programme isolé, mais bien un sous-
processus dérivé d’un processus parent, parfois appelé processus poids lourd (heavyweight process).
Un thread utilise le code de son processus parent. Il partage également l’espace d’adressage de son
processus parent et peut donc facilement partager des données avec celui-ci ou avec d’autres
threads. Par contre, chaque thread est exécuté indépendamment de son parent, il possède son
propre contexte d’exécution et éventuellement ses propres données.
• La gestion d’une interface graphique dans laquelle la gestion de chaque fenêtre est réalisée
par un thread différent.
• Un serveur Web dans lequel chaque requête HTTP est traitée par un thread séparé.
Avantages :
• L’échange de données entre différents threads est très efficace puisqu’ils partagent le même
espace d’adressage.
• Pas de duplication du code en mémoire : les threads partagent le même code.
• Changement de contextes plus rapides, car même mémoire virtuelle.
Inconvénients :
9 Les architectures à
9 processeurs multiples
9.1 Introduction
L’augmentation de la puissance de calcul des ordinateurs durant ces 50 dernières années a été
phénoménale. Cette puissance accrue nous permet de résoudre aujourd’hui des problèmes
extrêmement complexes au moyen d’un ordinateur, alors que ce n’était même pas imaginable il y a
seulement quelques dizaines d’années.
Face à ces nouveaux moyens toujours plus performants, l’imagination de l’homme est sans limites.
De nouvelles idées d’utilisation de l’outil informatique voient constamment le jour, toujours plus
gourmandes en puissance de calcul : décodage du génome humain, simulations numériques de plus
en plus sophistiquées…
Jusqu’à présent, l’amélioration des technologies existantes permettait d’assurer une augmentation
régulière des performances : intégration plus poussée, vitesse d’horloge plus rapide, processeur plus
efficace, mémoire cache et mémoire centrale plus grandes, etc.
Malheureusement, nous avons vu dans notre introduction au chapitre 1 que l’intégration de plus en
plus poussée se heurte aux limites physiques de la technologie actuelle (semi-conducteurs à base de
silicium). Sauf changement radical de la technologie utilisée, il n’est plus possible d’augmenter de
façon significative la puissance de calcul d’un processeur unique.
Une autre voie complémentaire est celle des processeurs spécialisés. Un processeur spécialisé est
conçu pour réaliser un type de tâches bien défini : cryptage de données, compression de données,
traitements multimédias, processeur réseau… Il est alors possible d’optimiser spécifiquement son
architecture en fonction des tâches à réaliser, ce qui n’est pas le cas pour un processeur généraliste.
Sur le plan matériel, on distinguera le parallélisme interne qui consiste à exécuter plusieurs tâches
simultanément au sein d’un seul processeur et le parallélisme externe dans lequel plusieurs
processeurs se répartissent les tâches en collaborant de manière plus ou moins étroite.
• Processeurs à cœurs (core) multiples. Chaque cœur est un processeur à part entière. Le fait
de les regrouper au sein d’un même circuit intégré permet une communication très rapide
entre les différents processeurs, ainsi que l’utilisation d’une mémoire cache partagée. Cette
approche permet à plusieurs cœurs de collaborer de manière très efficace à l’exécution
d’une tâche commune.
• Plusieurs processeurs dans un seul ordinateur (partage de la mémoire centrale uniquement).
• Plusieurs ordinateurs interconnectés par un réseau très rapide : grappe de serveurs (cluster),
grille informatique (grid computing).
Dans le cas du parallélisme transparent, l’utilisateur n’a pas à se préoccuper de la manière dont les
tâches sont exécutées en parallèle : le système d’exploitation organise l’exécution simultanée de
plusieurs processus, le compilateur optimise le code du programme pour tirer parti des spécificités
du processeur (par exemple utiliser plusieurs unités de calcul simultanément), le processeur
maximise l’exécution du programme grâce au pipelining et à la mémoire cache.
Le parallélisme explicite demande quant à lui que l’utilisateur conçoive son programme de manière
spécifique :
Le parallélisme explicite demande une approche radicalement différente dans la conception des
programmes, des algorithmes et des structures de données. C’est probablement le domaine de
l’informatique où il reste la plus grosse marge de manœuvre en termes d’amélioration des
performances.
• SISD (Single Instruction, Single Data stream) – C’est l’ordinateur classique, basé sur
l’architecture de Von Neumann. Il exécute une seule instruction à la fois (une seule unité de
contrôle) et produit un seul résultat à la fois (une seule unité arithmétique et logique). Le
seul parallélisme éventuel est interne, par exemple grâce à l’utilisation d’un pipeline.
• SIMD (Single Instruction, Multiple Data streams) – L’unité de contrôle va chercher les
instructions du programme une à la fois, mais elle dispose de plusieurs unités de calcul ce qui
lui permet d’exécuter plusieurs instructions en parallèle par recouvrement des cycles
d’exécution des instructions successives. Elle peut aussi exécuter la même instruction sur
plusieurs données simultanément. Les architectures super scalaires et vectorielles sont du
type SIMD. Les extensions MMX, SSE et AVX des processeurs Intel offrent également un
parallélisme de type SIMD.
• MISD (Multiple Instructions, Single Data stream) – Une telle architecture aurait pour
caractéristique d’appliquer simultanément plusieurs instructions à une même donnée. Il n’y
a pas d’exemple connu d’ordinateur basé sur cette architecture.
• MIMD (Multiple Instructions, Multiple Data streams) – Cette dernière catégorie contient
toutes les architectures composées de plusieurs processeurs travaillant de concert au sein
d’un système global.
La catégorie MIMD est relativement vaste. On distinguera au sein de cette catégorie les systèmes :
• multicœurs ou multiprocesseurs qui sont caractérisés par un niveau de couplage élevé. Les
différents processeurs sont connectés à un bus commun et ils partagent de la mémoire
centrale, voire même de la mémoire cache pour les systèmes multicœurs (mémoire
partagée).
• Multiordinateurs qui sont caractérisés par un couplage faible. Les différents ordinateurs
possèdent leur propre mémoire (mémoire distribuée). Ils sont interconnectés au moyen d’un
réseau de communication propriétaire à très grande vitesse (processeurs massivement
parallèles) ou basé sur des technologies standard telles que gigabit Ethernet (fermes de
serveurs).
Avec l’augmentation croissante des résolutions d’affichage, une image représente un volume de
données considérable. Ainsi, une image en résolution 4K (ultra haute définition) a une taille de 4096
× 3072 pixels, soit 12,6 mégapixels. Si chaque couleur rouge/vert/bleu est codée sur 8 bits, on
obtient un volume total de presque 40 Mo par image !
Un GPU part de la constatation que la génération ou le traitement d’une image consiste très souvent
à réaliser la même séquence d’opérations simultanément sur de nombreux points de l’image.
En simplifiant, on peut dire qu’un GPU est une architecture MIMD composée de nombreux
processeurs SIMD :
• Des unités de calcul distinctes peuvent effectuer simultanément des traitements différents
sur des portions distinctes d’une même image (traitement MIMD).
• Chaque unité de calcul est elle-même capable d’exécuter une même opération sur un grand
nombre de données simultanément (traitement SIMD) ;
• Enfin, l’architecture d’un GPU doit offrir une très grande bande passante entre la mémoire et
les unités de calcul afin de pouvoir acheminer l’énorme volume de données que représente
chaque image.
À titre d’illustration, nous reproduisons ci-dessous le schéma d’un GPU de chez NVIDIA basé sur
l’architecture Fermi.
Le GPU Fermi est constitué de 16 « streaming multiprocessor » (SM) qui partagent une mémoire
cache de niveau 2 (L2 Cache). Ce sont les 16 bandes verticales sur le schéma ci-dessous.
Chaque SM est lui-même constitué de 32 cœurs CUDA. Chaque cœur contient une unité
arithmétique et logique (ALU nombres entiers) et une unité FPU (Floating-Point Unit).
Note : les figures sont extraites du White Paper de NVIDIA « NVIDIA’s Next Generation CUDA™
Compute Architecture : Fermi™ »
Bibliographie
Le contenu de ce cours est principalement basé sur les livres de référence présentés ci-dessous.
La lecture d'un ou plusieurs de ces ouvrages vous permettra, si vous le souhaitez, d'approfondir vos
connaissances sur ce sujet passionnant que constitue l'architecture des ordinateurs.
Charles Petzold
« Code : The Hidden Language of Computer Hardware and Software »
first edition, Microsoft Press, 2000.
Nombres de 0 à 15 Puissances de 2, 8 et 16
Décimal Hexa Binaire Octal Binaire n 2n 8n 16n
0 0 0000 0 000 -4 0,0625 0,000244140625 -
1 1 0001 1 001 -3 0,125 0,001953125 0,000244140625
2 2 0010 2 010 -2 0,25 0,015625 0,00390625
3 3 0011 3 011 -1 0,5 0,125 0,0625
4 4 0100 4 100 0 1 1 1
5 5 0101 5 101 1 2 8 16
6 6 0110 6 110 2 4 64 256
7 7 0111 7 111 3 8 512 4.096
8 8 1000 - - 4 16 4.096 65.536
9 9 1001 - - 5 32 32.768 1.048.576
10 A 1010 - - 6 64 262.144 16.777.216
11 B 1011 - - 7 128 2.097.152 268.435.456
12 C 1100 - - 8 256 16.777.216 4.294.967.296
13 D 1101 - - 9 512 134.217.728 68.719.476.736
14 E 1110 - - 10 1.024 1.073.741.824 -
15 F 1111 - - 11 2.048 8.589.934.592 -
12 4.096 68.719.476.736 -
13 8.192 549.755.813.888 -
14 16.384 - -
15 32.768 - -
Code ASCII (7 bits) - extrait 16 65.536 - -