Architecture des ordinateurs · C3 Le processeur · Chapitre 2 · 10 h
Jeu d'instructions et assembleur
Format d'instruction, modes d'adressage et registres ; instructions arithmétiques, logiques et de branchement ; traduire un programme de haut niveau en assembleur.
Deux lignes de programme :
somme = 0pour i de 1 à 2 : somme = somme + iSept instructions machine, douze exécutions, et pas une seule variable — seulement des registres numérotés. C'est l'écart que ce chapitre doit combler, et il n'y a pas de raccourci : il se comble en traçant, pas en lisant. C'est pourquoi il pèse dix heures à lui seul, un cinquième du cours.
Le jeu d'instructions (instruction set architecture) est le contrat entre le matériel et le logiciel : la liste de ce que la machine sait faire, et la façon exacte de le lui demander. C'est la seule couche du cours qui soit à la fois visible du programmeur et gravée dans le silicium — ce qui explique sa stabilité. Un binaire x86 de 1995 s'exécute encore aujourd'hui.
Une instruction est un entier
Le chapitre 5 l'a posé : dans le registre d'instruction, il n'y a qu'un mot. Le format d'instruction dit comment ce mot se découpe. Prenons MIPS, l'architecture du TP, dont toutes les instructions tiennent sur exactement 32 bits.
Format R — opérations entre registres┌────────┬───────┬───────┬───────┬────────┬────────┐│ op 6 │ rs 5 │ rt 5 │ rd 5 │ sh 5 │ fn 6 │└────────┴───────┴───────┴───────┴────────┴────────┘ add $rd, $rs, $rt rd ← rs + rt Format I — une valeur immédiate ou un déplacement┌────────┬───────┬───────┬────────────────────────┐│ op 6 │ rs 5 │ rt 5 │ immédiat 16 │└────────┴───────┴───────┴────────────────────────┘ addi $rt, $rs, imm rt ← rs + imm Format J — un saut┌────────┬───────────────────────────────────────┐│ op 6 │ adresse 26 │└────────┴───────────────────────────────────────┘Trois enseignements se lisent directement sur ces cases.
Cinq bits pour désigner un registre, donc registres : le nombre n'est pas
choisi, il découle du format. Seize bits pour l'immédiat, donc une constante comprise entre
et ; charger une valeur plus grande demande deux instructions, ce qui surprend
toujours en TD. Et six bits de code opération, soit 64 codes — d'où le champ fn
supplémentaire du format R, qui démultiplie les possibilités sans élargir le mot.
C'est l'occasion de nommer un débat vieux de quarante ans. Une architecture CISC (x86) offre des instructions nombreuses, de longueurs variables, dont certaines font beaucoup de travail. Une architecture RISC (MIPS, ARM, RISC-V) n'offre que des instructions simples, toutes de même longueur, et compte sur le compilateur pour les combiner. Le second choix simplifie énormément le décodage et rend le pipeline du chapitre 8 possible ; c'est pourquoi il domine aujourd'hui, y compris à l'intérieur des processeurs x86, qui traduisent en interne leurs instructions vers un jeu de type RISC.
Les registres, et pourquoi ils existent
Un registre est une mémoire de la taille d'un mot, à l'intérieur du processeur. Il n'a pas d'adresse : il a un numéro, et ce numéro tient dans l'instruction elle-même.
Sa raison d'être est le goulot d'étranglement du chapitre 5. Lire un registre coûte une
fraction de cycle ; lire la mémoire en coûte plusieurs dizaines, voire des centaines si la
donnée n'est pas en cache. Une architecture RISC en tire une règle stricte, dite
chargement-rangement : seules deux instructions touchent la mémoire, lw pour charger
et sw pour ranger. Tout le calcul se fait entre registres.
Conséquence directe sur la façon d'écrire : on charge, on calcule autant que possible, on
range. Un TD où chaque opération commence par un lw et finit par un sw produit du code
juste et trois fois trop lent.
Quelques registres MIPS reviennent constamment : $zero, qui vaut toujours 0 et ne peut pas
être écrit — c'est lui qui permet de charger une constante par addi $t0, $zero, 5, faute
d'instruction « mettre à » ; $t0 à $t9, les registres de travail ; $s0 à $s7, ceux
qu'une fonction doit restituer intacte ; $sp, le sommet de pile ; $ra, l'adresse de retour.
Les modes d'adressage
Un opérande peut être désigné de plusieurs façons. Le mode d'adressage est cette façon, et c'est le second point qui coince du chapitre — moins par difficulté que par confusion entre « l'adresse » et « ce qui est à l'adresse ».
| Mode | Écriture | L'opérande est… |
|---|---|---|
| Immédiat | addi $t0, $zero, 5 | dans l'instruction elle-même |
| Registre | add $t0, $t1, $t2 | dans un registre |
| Direct (absolu) | lw $t0, 2000 | à l'adresse écrite dans l'instruction |
| Indirect par registre | lw $t0, 0($t1) | à l'adresse contenue dans un registre |
| Basé avec déplacement | lw $t0, 8($t1) | à l'adresse $t1 + 8 |
| Relatif au compteur ordinal | beq $t0, $t1, +12 | à l'adresse courante + un écart |
Deux méritent un mot. Le basé avec déplacement est le mode de l'accès aux tableaux et aux
champs de structure : l'adresse de base dans un registre, le décalage constant dans
l'instruction. T[i] pour un tableau d'entiers de 4 octets devient « adresse de T plus
» — d'où le décalage de 2 bits, plus rapide qu'une multiplication, que le compilateur
génère systématiquement.
Le relatif au compteur ordinal est le mode des branchements. On n'écrit pas l'adresse absolue de la cible mais l'écart par rapport à l'instruction courante, ce qui tient sur 16 bits et rend le code translatable : un programme chargé ailleurs en mémoire continue de fonctionner sans réécriture, ce dont le chargeur du cours de systèmes tire parti.
Quiz · 1 question
En MIPS, quelle est la différence entre addi $t0, $t1, 8 et lw $t0, 8($t1) ?
- Aucune : les deux ajoutent 8 au contenu de $t1 et rangent le résultat dans $t0 — identiques
- La première met la valeur $t1 + 8 dans $t0 ; la seconde va CHERCHER EN MÉMOIRE le mot situé à l'adresse $t1 + 8 et met ce mot dans $t0 — calcul contre accès mémoire
- La première travaille sur des entiers, la seconde sur des flottants — types différents
Réponse : Les deux calculent bien la même somme $t1 + 8, mais elles n'en font pas la même chose. addi s'arrête là : la SOMME est le résultat. lw traite cette somme comme une ADRESSE, effectue un accès mémoire, et rapporte le contenu trouvé. C'est toute la distinction entre l'adresse et ce qui est à l'adresse, et c'est la confusion la plus fréquente du chapitre. Deuxième conséquence, celle du chapitre 7 : addi coûte un cycle, lw peut en coûter plusieurs centaines si la donnée n'est pas en cache.
Quatre familles d'instructions
Arithmétiques et logiques. add, sub, and, or, xor, sll et srl pour les
décalages. Elles lisent des registres et écrivent un registre. Chacune correspond à un réglage
de l'UAL du chapitre 4, sélectionné par le multiplexeur que le décodage commande.
Transfert. lw et sw entre mémoire et registre, move entre registres. À noter, la
distinction annoncée au chapitre 2 : lb charge un octet avec extension de signe, lbu
sans. Choisir la mauvaise transforme un en 251, silencieusement.
Branchements et sauts. C'est ici que tout le contrôle se joue, et le mécanisme est
toujours le même : écrire dans le compteur ordinal. beq et bne le font sous condition
d'égalité, j inconditionnellement, jal en sauvegardant au passage l'adresse de retour dans
$ra — c'est l'appel de fonction, et jr $ra en est le retour.
Un détail qui déroute : MIPS n'a pas d'instruction « brancher si inférieur ». Il faut deux
temps — slt calcule un booléen dans un registre, puis beq ou bne branche dessus. C'est le
slti que l'animation ci-dessous exécute, et c'est aussi pourquoi la comparaison signée et la
non signée sont deux instructions distinctes, slt et sltu.
Appels système. syscall passe la main au système d'exploitation pour tout ce que le
processeur seul ne peut pas faire : afficher, lire, terminer. C'est la frontière que le
chapitre 1 du cours de systèmes appellera le passage en mode noyau.
Traduire du haut niveau
Toutes les structures de contrôle se ramènent à des branchements, selon des schémas mécaniques qu'il faut connaître par cœur.
si (a < b) alors X sinon Y slt $t0, $a, $b # $t0 ← (a < b) beq $t0, $zero, sinon # condition INVERSÉE : on saute si FAUX ... X ... j finsisinon: ... Y ...finsi:La règle qui surprend : la condition est toujours inversée. En haut niveau on entre dans le bloc si le test réussit ; en assembleur on saute par-dessus le bloc si le test échoue. Une fois cette inversion admise, les trois autres schémas s'écrivent seuls.
tant que (i < n) faire corps boucle: slt $t0, $i, $n beq $t0, $zero, fin ... corps ... j bouclefin:Un pour est un tant que dont l'initialisation précède et l'incrément clôt le corps — c'est
exactement le programme de l'animation. Et l'accès T[i] se décompose en trois temps : décaler
i de 2 bits pour obtenir l'écart en octets, l'ajouter à l'adresse de base, puis charger.
Animation · 14 étapes
somme = 0 ; pour i de 1 à 2 : somme = somme + i
- Initialiser l'accumulateur — addi ajoute une valeur immédiate à un registre. Additionner 0 au registre $zero, toujours nul, est la façon idiomatique de charger une constante : il n'y a pas d'instruction « mettre à ».
- Initialiser le compteur — Les variables de haut niveau somme et i n'existent plus : ce sont $t0 et $t1. C'est l'allocation de registres, et c'est le premier travail d'un compilateur.
- Tour 1 — évaluer la condition — slti (set less than immediate) écrit 1 dans $t2 si i est inférieur à 3, sinon 0. Le test d'une boucle de haut niveau devient donc DEUX instructions : calculer un booléen, puis brancher dessus.
- Tour 1 — le branchement n'est pas pris — beq saute si les deux registres sont égaux. $t2 vaut 1 et $zero vaut 0 : le saut n'a pas lieu, le CO passe simplement à l'instruction suivante. La condition est INVERSÉE par rapport au code source — on saute quand la boucle doit s'arrêter.
- Tour 1 — le corps de la boucle — Enfin l'addition utile : somme ← somme + i. Une seule instruction du programme source, une seule instruction machine — c'est l'exception, pas la règle.
- Tour 1 — incrémenter — L'incrément du « pour » est explicite en assembleur. Rien ne l'écrit à votre place, et l'oublier produit la boucle infinie la plus classique du TD.
- Tour 1 — retour en tête — j écrit une nouvelle valeur dans le compteur ordinal. Toute structure de contrôle — boucle, condition, appel de fonction — se ramène à cela : écrire dans le CO.
- Tour 2 — la condition tient encore — i vaut 2, donc i inférieur à 3 : $t2 reste à 1.
- Tour 2 — on n'en sort pas — Même branchement, même issue : le corps s'exécute une seconde fois.
- Tour 2 — somme vaut 3 — 1 + 2. L'accumulateur porte bien son nom.
- Tour 2 — incrémenter — i passe à 3. C'est cette valeur qui fera échouer le test au tour suivant.
- Tour 2 — retour en tête — Troisième évaluation de la condition, et cette fois elle va tomber.
- Tour 3 — la condition tombe — 3 n'est pas inférieur à 3 : slti écrit 0 dans $t2. Le corps ne s'exécutera pas une troisième fois, ce qui est bien le comportement du « pour i de 1 à 2 » de départ.
- Sortie de boucle — $t2 vaut 0, donc égal à $zero : le branchement est pris et le CO saute à l'étiquette fin. somme vaut 3, soit 1 + 2. Le programme de haut niveau tenait en deux lignes ; il en a fallu sept, et douze exécutions d'instruction.
Quiz · 1 question
Dans la traduction d'un tant que (i < n), pourquoi le branchement teste-t-il l'échec de la condition plutôt que sa réussite ?
- Par convention historique du langage assembleur MIPS — convention
- Parce qu'un branchement fait SAUTER : pour continuer dans la boucle il ne faut rien faire, et il faut sauter uniquement pour en sortir — donc quand la condition est fausse — sauter, c'est sortir
- Parce que les processeurs évaluent plus rapidement une condition fausse qu'une condition vraie — vitesse d'évaluation
Réponse : En assembleur il n'existe pas de « bloc » : il n'y a que des instructions consécutives et des sauts. Continuer dans le corps de la boucle est donc l'action PAR DÉFAUT, celle qui ne demande aucune instruction. Le saut ne sert qu'à l'exception : quitter la boucle. On branche donc quand la condition d'entrée est FAUSSE. Cette inversion est le réflexe à acquérir — une fois admise, si-sinon, tant que et pour s'écrivent mécaniquement. Elle explique aussi pourquoi le code compilé teste souvent le contraire de ce qu'on a écrit.
À vous
L'exercice donne un interpréteur MIPS réduit — une quinzaine d'instructions — et vous demande d'écrire le programme, pas la machine. Trois traductions à produire, dans l'ordre de difficulté : un maximum de deux valeurs (si-sinon), la somme d'un tableau (boucle et accès indexé), puis un compte d'éléments pairs.
L'interpréteur affiche la trace registre par registre. Servez-vous-en comme du tableau de la salle de TD : quand un programme ne donne pas le bon résultat, la trace montre l'instruction exacte où l'état diverge de ce que vous attendiez.
Exercice de code
Traduisez un si-sinon puis une boucle de haut niveau en assembleur MIPS réduit.
Point de départ
// ── Un interpréteur MIPS réduit ───────────────────────────────────────────
// Une instruction : [mnémonique, destination, source1, source2].
// Registres nommés librement ; "zero" vaut toujours 0.
function executer(prog, memoire, tracer) {
const R = { zero: 0 };
const lire = (x) => (typeof x === "number" ? x : (R[x] ?? 0));
const etiquettes = {};
prog.forEach((ins, i) => { if (ins[0] === "label") etiquettes[ins[1]] = i; });
let CO = 0, pas = 0;
while (CO < prog.length && pas++ < 500) {
const [op, a, b, c] = prog[CO];
let saut = null;
if (op === "li") R[a] = lire(b);
if (op === "add") R[a] = lire(b) + lire(c);
if (op === "sub") R[a] = lire(b) - lire(c);
if (op === "addi") R[a] = lire(b) + c;
if (op === "slt") R[a] = lire(b) < lire(c) ? 1 : 0;
if (op === "slti") R[a] = lire(b) < c ? 1 : 0;
if (op === "andi") R[a] = lire(b) & c;
if (op === "lw") R[a] = memoire[lire(b) + c]; // lw a, c(b)
if (op === "sw") memoire[lire(b) + c] = lire(a);
if (op === "beq" && lire(a) === lire(b)) saut = etiquettes[c];
if (op === "bne" && lire(a) !== lire(b)) saut = etiquettes[c];
if (op === "j") saut = etiquettes[a];
if (tracer && op !== "label") {
console.log(" " + [op, a, b, c].filter((x) => x !== undefined).join(" ").padEnd(22) +
JSON.stringify(R));
}
CO = saut !== null && saut !== undefined ? saut : CO + 1;
}
return R;
}
// ── 1. Maximum de deux valeurs (si-sinon) ─────────────────────────────────
// max = (a < b) ? b : a, avec a en $a et b en $b, résultat dans $max.
const MAX = [
["li", "a", 12], ["li", "b", 37],
["slt", "t", "a", "b"], // t = (a < b)
["beq", "t", "zero", "sinon"], // condition INVERSÉE
// ← branche « alors » : max = b
["j", "fin"],
["label", "sinon"],
// ← branche « sinon » : max = a
["label", "fin"],
];
// ── 2. Somme d'un tableau (boucle + accès indexé) ─────────────────────────
// Le tableau occupe la mémoire à partir de l'adresse 0, un mot par case.
const TABLEAU = [4, 8, 15, 16, 23, 42];
const SOMME = [
["li", "somme", 0],
["li", "i", 0],
["li", "n", TABLEAU.length],
["label", "boucle"],
// ← à écrire : sortir si i >= n, charger T[i], accumuler, incrémenter i
];
// ── À VOUS ────────────────────────────────────────────────────────────────
// Complétez MAX puis SOMME. Un troisième défi : compter les valeurs paires
// du tableau (indice : andi t, x, 1 met à 1 le bit de parité).
console.log("— maximum —");
console.log("max =", executer(MAX, [], true).max, "| attendu 37");
console.log("— somme —");
console.log("somme =", executer(SOMME, [...TABLEAU], false).somme, "| attendu 108");
Solution
function executer(prog, memoire, tracer) {
const R = { zero: 0 };
const lire = (x) => (typeof x === "number" ? x : (R[x] ?? 0));
const etiquettes = {};
prog.forEach((ins, i) => { if (ins[0] === "label") etiquettes[ins[1]] = i; });
let CO = 0, pas = 0;
while (CO < prog.length && pas++ < 500) {
const [op, a, b, c] = prog[CO];
let saut = null;
if (op === "li") R[a] = lire(b);
if (op === "add") R[a] = lire(b) + lire(c);
if (op === "sub") R[a] = lire(b) - lire(c);
if (op === "addi") R[a] = lire(b) + c;
if (op === "slt") R[a] = lire(b) < lire(c) ? 1 : 0;
if (op === "slti") R[a] = lire(b) < c ? 1 : 0;
if (op === "andi") R[a] = lire(b) & c;
if (op === "lw") R[a] = memoire[lire(b) + c];
if (op === "sw") memoire[lire(b) + c] = lire(a);
if (op === "beq" && lire(a) === lire(b)) saut = etiquettes[c];
if (op === "bne" && lire(a) !== lire(b)) saut = etiquettes[c];
if (op === "j") saut = etiquettes[a];
if (tracer && op !== "label") {
console.log(" " + [op, a, b, c].filter((x) => x !== undefined).join(" ").padEnd(22) +
JSON.stringify(R));
}
CO = saut !== null && saut !== undefined ? saut : CO + 1;
}
return R;
}
// 1. Maximum. Le schéma si-sinon : tester, brancher sur l'ÉCHEC vers la
// branche sinon, écrire la branche alors, sauter par-dessus le sinon.
const MAX = [
["li", "a", 12], ["li", "b", 37],
["slt", "t", "a", "b"],
["beq", "t", "zero", "sinon"],
["add", "max", "b", "zero"], // max = b (pas d'instruction « copier »)
["j", "fin"],
["label", "sinon"],
["add", "max", "a", "zero"], // max = a
["label", "fin"],
];
const TABLEAU = [4, 8, 15, 16, 23, 42];
// 2. Somme. Le schéma tant que : condition en tête, saut de sortie sur
// l'échec, corps, incrément, saut de retour.
const SOMME = [
["li", "somme", 0],
["li", "i", 0],
["li", "n", TABLEAU.length],
["label", "boucle"],
["slt", "t", "i", "n"],
["beq", "t", "zero", "fin"],
["lw", "x", "i", 0], // ici une case = un mot, donc pas de x4
["add", "somme", "somme", "x"],
["addi", "i", "i", 1],
["j", "boucle"],
["label", "fin"],
];
// 3. Compter les valeurs paires : même boucle, un test de parité en plus.
const PAIRS = [
["li", "compte", 0], ["li", "i", 0], ["li", "n", TABLEAU.length],
["label", "boucle"],
["slt", "t", "i", "n"],
["beq", "t", "zero", "fin"],
["lw", "x", "i", 0],
["andi", "p", "x", 1], // p = bit de poids faible = 1 si impair
["bne", "p", "zero", "suite"], // impair : on ne compte pas
["addi", "compte", "compte", 1],
["label", "suite"],
["addi", "i", "i", 1],
["j", "boucle"],
["label", "fin"],
];
console.log("— maximum —");
console.log("max =", executer(MAX, [], true).max, "| attendu 37");
console.log("— somme —");
console.log("somme =", executer(SOMME, [...TABLEAU], false).somme, "| attendu 108");
console.log("— nombre de valeurs paires —");
console.log("compte =", executer(PAIRS, [...TABLEAU], false).compte, "| attendu 4");
En travaux pratiques
Travaux pratiques 6 · 4 h
Traduire, à la main puis avec le compilateur
Écrire de l'assembleur, retrouver ses propres constructions dans ce que produit gcc, et découvrir que la convention d'appel n'est pas une règle du langage mais un contrat entre fonctions.
Avant de commencer
- Le TP 5 : cycle et compteur ordinal
- Un simulateur MIPS ou RISC-V (MARS, rars) et gcc
Énoncé
- Premiers pas — Dans le simulateur, écrivez un programme qui additionne deux constantes et affiche le résultat. Exécutez pas à pas et regardez les registres changer.
- Traduire une condition — Traduisez à la main un si-sinon en assembleur. Comptez le nombre d'étiquettes nécessaires et dites pourquoi la traduction inverse la condition. Indice : Il n'y a pas d'instruction « si vrai, exécute ce bloc » : seulement « si faux, saute ailleurs ».
- Traduire une boucle — Traduisez une boucle qui additionne les entiers de 1 à n. Faites-la tourner pour n = 5, puis comptez les instructions exécutées.
- Confronter au compilateur — Écrivez la même boucle en C, compilez avec gcc -S -O0, et comparez ligne à ligne avec votre version. Notez trois différences et expliquez-les.
- Appeler une fonction — Écrivez une fonction assembleur qui calcule un carré, appelez-la deux fois de suite. Respectez la convention : arguments, valeur de retour, adresse de retour.
- Casser la convention — Rendez votre fonction récursive sans sauvegarder l'adresse de retour. Exécutez et expliquez précisément où le programme part.
- Le prix de l'optimisation — Recompilez le C avec -O2 et comparez au -O0. Comptez les instructions et les accès mémoire, et repérez ce que le compilateur a supprimé.
C'est réussi quand
- Votre si-sinon se traduit avec la condition inversée, et vous savez dire pourquoi
- Votre fonction s'appelle deux fois de suite sans corrompre l'appelant
- La version récursive sans sauvegarde boucle, et vous savez sur quelle instruction
- Vous savez nommer une variable que -O2 a fait disparaître de la mémoire
Correction
/* en C */ # en assembleur
if (a > b) bgt $t0, $t1, vrai
c = 1; li $t2, 0 # sinon
else j fin
c = 0; vrai:
li $t2, 1
fin:Le processeur ne sait pas « exécuter un bloc si » : il sait sauter. Toute structure de contrôle se réécrit donc en tests et sauts, et souvent avec la condition INVERSÉE — on saute par-dessus le cas qu'on ne veut pas. C'est pourquoi un assembleur désassemblé est si pénible à relire : la structure du programme d'origine a disparu.
# votre version gcc -S -O0 li $t0, 1 # i sw $zero, -8($fp) # tout en MÉMOIRE li $t1, 0 # somme lw $eax, -8($fp) boucle: addl ... bgt $t0, $a0, fin sw $eax, -8($fp) # relu, réécrit add $t1, $t1, $t0 addi $t0, $t0, 1 j boucle fin:
Sans optimisation, gcc range CHAQUE variable en mémoire et la relit à chaque usage : c'est ce qui rend le pas-à-pas au débogueur fidèle au code source. Votre version, qui garde tout en registres, est plus rapide — et c'est exactement ce que fera -O2. Le compilateur n'est pas naïf : à -O0, il est délibérément littéral.
carre: mul $v0, $a0, $a0 # argument dans $a0, retour dans $v0 jr $ra # saute à l'adresse rangée dans $ra main: li $a0, 5 jal carre # jal range l'adresse SUIVANTE dans $ra move $s0, $v0
Rien dans le matériel n'impose que l'argument passe par $a0 : c'est une CONVENTION, respectée par tous les compilateurs d'une même plateforme. C'est elle qui permet d'appeler depuis du C une fonction écrite en assembleur, ou en Rust. La convention est la vraie interface binaire — pas le langage.
fact:
ble $a0, 1, base
addi $a0, $a0, -1
jal fact # ← ÉCRASE $ra avec l'adresse d'ici
...
base:
jr $ra # revient à l'appel INTERNE, pas à l'appelant
# → boucle infinie entre fact et lui-même
/* la correction */
addi $sp, $sp, -4
sw $ra, 0($sp) # empiler avant l'appel
...
lw $ra, 0($sp) # dépiler après
addi $sp, $sp, 4Il n'y a qu'un seul registre $ra, et chaque jal l'écrase. La pile n'est pas un confort de langage : c'est la seule façon de rendre la récursivité possible sur une machine à registres en nombre fini. Vous retrouverez exactement cette pile, vue d'en haut, au premier chapitre d'Algorithmique 2.
-O0 : 24 instructions, 12 accès mémoire -O2 : 7 instructions, 0 accès mémoire (hors résultat) la variable de boucle n'existe plus en mémoire : elle vit dans un registre du début à la fin et si n est une constante, gcc calcule la somme À LA COMPILATION
Une variable locale peut n'avoir aucune existence à l'exécution. C'est déroutant au débogueur — gdb affiche alors « optimized out » — et c'est la raison pour laquelle on débogue en -O0 et on livre en -O2. Cela explique aussi le TP 2 : le compilateur raisonne sur ce que la NORME autorise, pas sur ce que la machine ferait.
Ce que la suite en fait
Deux fils partent d'ici. Le premier va au chapitre 7 : lw et sw sont les instructions dont
le coût est le plus variable, de un cycle à plusieurs centaines selon que la donnée est en
cache ou non, et le principe de localité qui rend le cache efficace se lit directement dans les
schémas de boucle qu'on vient d'écrire.
Le second va au chapitre 8. Un jeu d'instructions RISC — longueur fixe, décodage simple, accès mémoire cantonné à deux instructions — n'est pas seulement élégant : c'est ce qui rend le pipeline réalisable. Et les branchements, dont on vient de voir qu'ils sont partout, y deviendront le principal obstacle, d'où la prédiction de branchement.
À retenir
Flashcards · 5 cartes
- Que peut-on déduire du seul format d'une instruction MIPS sur 32 bits ?
- Cinq bits par champ de registre, donc 32 registres — le nombre découle du format, il n'est pas choisi. Seize bits d'immédiat, donc des constantes de −32768 à 32767 seulement : au-delà il faut deux instructions. Six bits de code opération, soit 64 codes, complétés par un champ fn dans le format R pour ne pas élargir le mot.
- Qu'est-ce qu'une architecture chargement-rangement, et quelle discipline impose-t-elle ?
- Une architecture où SEULES deux instructions touchent la mémoire, lw et sw ; tout le calcul se fait entre registres. La discipline qui en découle : charger une fois, calculer autant que possible en registres, ranger une fois. Un code qui relit la mémoire à chaque opération est juste mais plusieurs fois trop lent, car un accès mémoire coûte des dizaines de cycles contre une fraction pour un registre.
- Quelle est la différence entre addi $t0, $t1, 8 et lw $t0, 8($t1) ?
- Les deux calculent $t1 + 8. addi s'arrête là : la somme EST le résultat, rangée dans $t0. lw traite cette somme comme une ADRESSE, fait un accès mémoire et rapporte le contenu trouvé. C'est la distinction entre l'adresse et ce qui est à l'adresse. Le mode « basé avec déplacement » de lw est celui de l'accès aux tableaux et aux champs de structure.
- Pourquoi la condition est-elle inversée quand on traduit un si ou un tant que en assembleur ?
- Parce qu'il n'existe pas de bloc en assembleur : seulement des instructions consécutives et des sauts. Continuer est l'action par défaut, gratuite ; le saut sert uniquement à l'exception, c'est-à-dire à passer par-dessus le bloc ou à sortir de la boucle. On branche donc quand la condition d'entrée est FAUSSE.
- Comment MIPS traite-t-il une comparaison « inférieur à », et pourquoi en deux temps ?
- Il n'a pas de « brancher si inférieur ». slt (ou slti) écrit d'abord un booléen 0/1 dans un registre, puis beq ou bne branche sur ce registre. D'où deux instructions pour un seul test de haut niveau. C'est aussi pourquoi slt et sltu sont distinctes : la comparaison signée et la non signée ne donnent pas le même résultat sur les mêmes bits, comme au chapitre 2.