Cours 1: Jeu d'instruction.
Quelles instructions un jeu d'instruction de processeur généraliste doit-il
offrir? Quelle doit être la représentation matérielle de ces instructions?
Voici le jeu d'instruction MIPS représentatif des architectures
chargement/rangement.
Les instructions se déclinent en trois formats décrits dans la figure ci-dessous.
Les instructions de type R sont des instructions à deux sources et une
destination en registres. Ce sont les instructions de calculs entiers
et flottants. Parmi ces calculs, on trouve les comparaisons de registres,
dont le résultat booléen est écrit dans le registre de destination.
Les instructions de type I ont une source en registre et une autre
(la source droite) qui est une constante 16-bits signée. Ce sont
les calculs entiers avec constante (par exemple i++), les chargements
(les sources additionnées forment l'adresse virtuelle d'accès) et les
rangements (le champ servant au registre de destination sert dans ce cas
à désigner le registre à ranger en mémoire). Il y a aussi les sauts
conditionnels (la constante est le déplacement signé du saut, relatif à PC,
le champ de destination n'est pas utilisé), les initialisations
de registres (la constante est placée en moitié haute ou basse) et enfin
les sauts indirects (la constante et le champ de destination ne sont pas
utilisés, l'adresse de retour des sauts avec lien est rangée dans R31).
Les instructions du type J n'ont pas de destination explicite. Elles
ont une source constante de 26-bits signée. Ce sont les instructions de
sauts inconditionnels immédiats. La source représente le déplacement signé
du saut, relatif à PC. Les sauts avec lien rangent leur adresse de retour
dans R31.
L'architecture MIPS définit 32 registres entiers (le registre R0 est la
constante 0, le registre R31 contient l'adresse de retour) et 32 registres
flottants simple précision, que l'on peut combiner par paire de registres
adjacents pour former 16 registres double précision.
La liste exhaustive des instructions MIPS et leur codage peut être
consultée ici .
A titre d'exemple, voici un programme C.
#include <stdio.h>
int main (int argc, char *argv[]) {
int i;
int somme = 0;
for (i=0; i<=100; i++) somme += (i*i);
printf("la somme des carrés des 100 premiers entiers est %d\n", somme);
}
Voici sa traduction en assembleur MIPS.
.text
.align 2 // alignment 2^2 bytes
.globl main
main:
subu $sp, $sp, 32 // R29 (stack pointer); allocate the frame
sw $ra, 20($sp) // R31; push the return address
sw $0, 24($sp) // R0 (constant 0); somme = 0;
sw $0, 28($sp) // i = 0;
loop:
lw $t6, 28($sp) // R14 (temporary 6; not pushed); i
mul $t7, $t6, $t6 // R15 (temporary 7; not pushed); i*i
lw $t8, 24($sp) // R24 (temporary 8; not pushed); somme
addu $t9, $t8, $t7 // R25 (temporary 9; not pushed); somme + i*i
sw $t9, 24($sp) // somme += (i*i);
addu $t0, $t6, 1 // R8 (temporary 0; not pushed); i+1
sw $t0, 28($sp) // i++;
ble $t0, 100, loop // if (i<=100) goto loop;
la $a0, str // R4 (argument 0); "la somme ..."
lw $a1, 24($sp) // R5 (argument 1); somme
jal printf // printf("la somme ...", somme);
move $v0, $0 // R2 = 0 (result value 0 is 0)
lw $ra, 20($sp) // pop return address
addu $sp, $sp, 32 // free function stack frame
jr $ra // return
.data
.align 0 // alignment disabled
str:
.asciiz "la somme des carrés des 100 premiers entiers est %d\n"
Voici une version légèrement différente (la borne de la boucle est une variable).
/*
#include <stdio.h>
int main (int argc, char *argv[]) {
int i, n;
int somme = 0;
printf("n: ");
scanf("%d", &n);
for (i=0; i<n; i++) somme += (i*i);
printf("la somme des carrés des %d premiers entiers est %d\n", n, somme);
}
*/
.text
.align 2
.globl main
main:
subu $sp, $sp, 32
sw $ra, 16($sp)
sw $0, 24($sp) // somme = 0;
sw $0, 28($sp) // i = 0;
la $a0, str1 // "n: "
jal printf // printf("n: ");
la $a0, str2
la $a1, 20($sp) // &n
jal scanf // scanf("%d", &n);
lw $t5, 20($sp) // n
beq $t5, $0, e0 // if (n==0) goto e0;
loop:
lw $t6, 28($sp) // i
mul $t7, $t6, $t6 // i*i
lw $t8, 24($sp) // somme
addu $t9, $t8, $t7 // somme + i*i
sw $t9, 24($sp) // somme += (i*i);
addu $t0, $t6, 1 // i+1
sw $t0, 28($sp) // i++;
ble $t0, $t5, loop // if (i<=n) goto loop;
e0:
la $a0, str3 // "la somme ..."
lw $a1, 20($sp) // n
lw $a2, 24($sp) // somme
jal printf // printf("la somme ...", n, somme);
move $v0, $0
lw $ra, 16($sp)
addu $sp, $sp, 32
jr $ra
.data
.align 0
str1:
.asciiz "n: "
str2:
.asciiz "%d"
str3:
.asciiz "la somme des carrés des 100 premiers entiers est %d\n"
Voici deux traductions différentes d'un même code. La première emploie un
saut conditionnel (ce qui n'est pas bon pour l'ILP) et la seconde emploie
une instruction de move conditionnel, apparue dans les versions
récentes du jeu d'instructions MIPS (remarque: les données étant unsigned , on effectue une comparaison non signée ( bgeu )).
/*
unsigned int z;
main() {
unsigned int x, y;
scanf("%d", &x);
scanf("%d", &y);
if (x<y)
z = x;
else
z = y;
printf("%d\n", z);
}
*/
// première version: saut conditionnel
.text
.align 2
.globl main
main:
subu $sp, $sp, 32
sw $ra, 20($sp)
la $a0, str1
la $a1, 24($sp) // &x
jal scanf // scanf("%d", &x);
la $a0, str1
la $a1, 28($sp) // &y
jal scanf // scanf("%d", &y);
lw $t6, 24($sp) // x
lw $t7, 28($sp) // y
bgeu $t6, $t7, e0 // if (x>=y) goto e0;
sw $t6, z // z = x;
b e1 // goto e1;
e0:
sw $t7, z // z = y;
e1:
la $a0, str2
lw $a1, z
jal printf // printf("%d\n", z);
move $v0, $0
lw $ra, 20($sp)
addu $sp, $sp, 32
jr $ra
.data
.align 0
str1:
.asciiz "%d"
str2:
.asciiz "%d\n"
.align 2
z:
.space 4 // reserve 4 bytes for z
// deuxième version: move conditionnel
.text
.align 2
.globl main
main:
subu $sp, $sp, 32
sw $ra, 20($sp)
la $a0, str1
la $a1, 24($sp) // &x
jal scanf // scanf("%d\n", &x);
la $a0, str1
la $a1, 28($sp) // &y
jal scanf // scanf("%d\n", &y);
lw $t6, 24($sp) // x
lw $t7, 28($sp) // y
sltu $t8, $t6, $t7 // t8 = (x<y)
movn $t9, $t6, $t8 // if (t8!=0) t9 = x
movz $t9, $t7, $t8 // if (t8==0) t9 = y
sw $t9, z // z = (x<y)?x:y;
la $a0, str2
lw $a1, z
jal printf // printf("%d\n", z);
move $v0, $0
lw $ra, 20($sp)
addu $sp, $sp, 32
jr $ra
.data
.align 0
str1:
.asciiz "%d"
str2:
.asciiz "%d\n"
.align 2
z:
.space 4
Voici un calcul de racine carrée qui implémente un while .
/*
#include <stdio.h>
int main (int argc, char *argv[]) {
unsigned int c = 0, i = 1, n;
printf("n: ");
scanf("%d", &n);
while (c<n) {
c += i;
i += 2;
}
printf("la racine carrée de %d est %d\n", n, i-1);
}
*/
.text
.align 2
.globl main
main:
subu $sp, $sp, 32
sw $ra, 16($sp)
sw $0, 24($sp) // c = 0;
li $t0, 1
sw $t0, 28($sp) // i = 1;
la $a0, str1
jal printf // printf("n: ");
la $a0, str2
la $a1, 20($sp) // &n
jal scanf // scanf("%d", &n);
lw $t5, 20($sp) // n
beq $t5, $0, e0 // if (n==0) goto e0;
loop:
lw $t6, 28($sp) // i
lw $t7, 24($sp) // c
addu $t7, $t7, $t6 // c+i
sw $t7, 24($sp) // c += i;
addu $t6, $t6, 2 // i+2
sw $t6, 28($sp) // i += 2;
bltu $t7, $t5, loop // if (c<n) goto loop;
e0:
la $a0, str3 // "la racine ..."
lw $a1, 20($sp) // n
lw $a2, 24($sp) // i
subu $a2, $a2, 1 // i-1
jal printf // printf("la racine ...", n, i-1);
move $v0, $0
lw $ra, 16($sp)
addu $sp, $sp, 32
jr $ra
.data
.align 0
str1:
.asciiz "n: "
str2:
.asciiz "%d"
str3:
.asciiz "la racine carrée de %d est %d\n"
Voici un exemple de fonction récursive avec un paramètre fonction engendrant
une instruction d'appel indirect jalr .
/*
#include <stdio.h>
ecrire(unsigned int n, unsigned int d, unsigned int a) {
printf("le disque %d va du piquet %d au piquet %d\n", n, d, a);
}
hanoi(unsigned int n, unsigned int d, unsigned int a, unsigned int i, int (*f)()) {
if (n==0) return;
hanoi(n-1, d, i, a, f);
f(n, d, a);
hanoi(n-1, i, a, d, f);
}
int main (int argc, char *argv[]) {
unsigned int n;
printf("n: ");
scanf("%d", &n);
hanoi(n, 1, 3, 2, ecrire);
}
*/
.text
.align 2
.globl main
main:
subu $sp, $sp, 32
sw $ra, 24($sp)
la $a0, str1
jal printf // printf("n: ");
la $a0, str2
la $a1, 28($sp) // &n
jal scanf // scanf("%d", &n);
lw $a0, 28($sp) // n
li $a1, 1 // 1
li $a2, 3 // 3
li $a3, 2 // 2
li $a4, ecrire // ecrire
jal hanoi // hanoi(n, 1, 3, 2, ecrire);
move $v0, $0
lw $ra, 24($sp)
addu $sp, $sp, 32
jr $ra
ecrire:
la $a0, str3 // "le disque ..."
move $a3, $a2 // a
move $a2, $a1 // d
move $a1, $a0 // n
jal printf // printf("le disque ...", n, d, a);
jr $ra
hanoi:
beqz $a0, e0 // if (n==0) goto e0;
subu $sp, $sp, 32
sw $ra, 24($sp)
addiu $a0, $a0, -1 // n-1
move $t0, $a2
move $a2, $a3 // i
move $a3, $t0 // a
jal hanoi // hanoi(n-1, d, i, a, ecrire);
move $t0, $a2
move $a2, $a3 // a
move $a3, $t0 // i
addiu $a0, $a0, 1 // n
jalr $a5 // ecrire(n, d, a);
addiu $a0, $a0, -1 // n-1
move $t0, $a1
move $a1, $a3 // i
move $a3, $t0 // d
jal hanoi // hanoi(n-1, i, a, d, ecrire);
lw $ra, 24($sp)
addu $sp, $sp, 32
e0:
jr $ra
.data
.align 0
str1:
.asciiz "n: "
str2:
.asciiz "%d"
str3:
.asciiz "le disque %d va du piquet %d au piquet %d\n"
Voici un tableau qui montre quelles sont les fréquences d'utilisation des
instructions MIPS dans les programmes de la suite de benchmarks SPEC2000.
Fréquence d'utilisation des instructions MIPS dans SPEC2000
| |
Fréquence dans SPECint |
Fréquence dans SPECfp |
| instr |
gap |
gcc |
gzip |
mcf |
perl |
moyen |
applu |
art |
equak |
lucas |
swim |
moyen |
| load |
26,5% |
25,1% |
20,1% |
30,3% |
28,7% |
26,0% |
13,8% |
18,1% |
22,3% |
10,6% |
9,1% |
15,0% |
| store |
10,3% |
13,2% |
5,1% |
4,3% |
16,2% |
10,0% |
2,9% |
0,0% |
0,8% |
3,4% |
1,3% |
2,0% |
| add |
21,1% |
19,0% |
26,9% |
10,1% |
16,7% |
19,0% |
30,4% |
30,1% |
17,4% |
11,1% |
24,4% |
23,0% |
| sub |
1,7% |
2,2% |
5,1% |
3,7% |
2,5% |
3,0% |
2,5% |
0,0% |
0,1% |
2,1% |
3,8% |
2,0% |
| mul |
1,4% |
0,1% |
0,0% |
0,0% |
0,0% |
0,0% |
2,3% |
0,0% |
0,0% |
1,2% |
0,0% |
1,0% |
| comp |
2,8% |
6,1% |
6,6% |
6,3% |
3,8% |
5,0% |
0,0% |
7,4% |
2,1% |
0,0% |
0,0% |
2,0% |
| init |
4,8% |
2,5% |
1,5% |
0,1% |
1,7% |
2,0% |
13,7% |
0,0% |
1,0% |
1,8% |
9,4% |
5,0% |
| bcond |
9,3% |
12,1% |
11,0% |
17,5% |
10,9% |
12,0% |
2,5% |
11,5% |
2,9% |
0,6% |
1,3% |
4,0% |
| cmov |
0,4% |
0,6% |
1,1% |
0,1% |
1,9% |
1,0% |
0,0% |
0,3% |
0,1% |
0,0% |
0,0% |
0,0% |
| jump |
0,8% |
0,7% |
0,8% |
0,7% |
1,7% |
1,0% |
0,0% |
0,0% |
0,1% |
0,0% |
0,0% |
0,0% |
| call |
1,6% |
0,6% |
0,4% |
3,2% |
1,1% |
1,0% |
0,0% |
0,0% |
0,7% |
0,0% |
0,0% |
0,0% |
| ret |
1,6% |
0,6% |
0,4% |
3,2% |
1,1% |
1,0% |
0,0% |
0,0% |
0,7% |
0,0% |
0,0% |
0,0% |
| shift |
3,8% |
1,1% |
2,1% |
1,1% |
0,5% |
2,0% |
0,7% |
0,0% |
0,2% |
1,9% |
0,0% |
1,0% |
| and |
4,3% |
4,6% |
9,4% |
0,2% |
1,2% |
4,0% |
0,0% |
0,0% |
0,2% |
1,8% |
0,0% |
0,0% |
| or |
7,9% |
8,5% |
4,8% |
17,6% |
8,7% |
9,0% |
0,8% |
1,1% |
2,3% |
1,0% |
7,2% |
2,0% |
| xor |
1,8% |
2,1% |
4,4% |
1,5% |
2,8% |
3,0% |
0,0% |
3,2% |
0,1% |
0,0% |
0,0% |
1,0% |
| logiq |
0,1% |
0,4% |
0,1% |
0,1% |
0,3% |
0,0% |
0,0% |
0,0% |
0,1% |
0,0% |
0,0% |
0,0% |
| fload |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
11,4% |
12,0% |
19,7% |
16,2% |
16,8% |
15,0% |
| fstor |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
4,2% |
4,5% |
2,7% |
18,2% |
5,0% |
7,0% |
| fadd |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
2,3% |
4,5% |
9,8% |
8,2% |
9,0% |
7,0% |
| fsub |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
2,9% |
0,0% |
1,3% |
7,6% |
4,7% |
3,0% |
| fmul |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
8,6% |
4,1% |
12,9% |
9,4% |
6,9% |
8,0% |
| fdiv |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,3% |
0,6% |
0,5% |
0,0% |
0,3% |
0,0% |
| fmov |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,7% |
0,9% |
1,2% |
1,8% |
0,9% |
1,0% |
| fcomp |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,9% |
0,6% |
0,8% |
0,0% |
0,0% |
| fcmov |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,6% |
0,0% |
0,8% |
0,0% |
0,0% |
| fautre |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
0,0% |
1,6% |
0,0% |
0,0% |
Voici une synthèse de ces résultats.
La première colonne min représente le plus petit taux parmi ceux des 5 benchmarks
de SPECint.
La
seconde colonne min est le plus petit taux parmi ceux des 5 benchmarks
de SPECfp et la troisième colonne min est le plus petit taux parmi les 10 de
SPEC2000.
La première colonne max est le plus grand des 5 de SPECint.
La seconde colonne max est le plus grand des 5 taux de SPECfp.
La troisième colonne max est le plus grand des 10 taux de SPEC2000.
Enfin, les trois colonnes moy sont les taux moyens pour SPECint, SPECfp et SPEC2000.
Fréquence d'utilisation des instructions MIPS dans SPEC2000
| |
Fréquence dans SPECint |
Fréquence dans SPECfp |
Fréquence dans SPEC2000 |
| catégorie |
min |
max |
moy |
min |
max |
moy |
min |
max |
moy |
| accès mém |
25,2% |
44,9% |
36,0% |
32,2% |
48,4% |
39,0% |
25,2% |
48,4% |
37,5% |
| add/sous |
20,1% |
38,6% |
27,0% |
13,2% |
37,5% |
27,0% |
13,2% |
38,6% |
27,0% |
| move |
0,2% |
5,2% |
3,0% |
0,3% |
13,7% |
5,0% |
0,2% |
13,7% |
4,0% |
| op logiq |
13,0% |
19,4% |
16,0% |
0,8% |
7,2% |
3,0% |
0,8% |
19,4% |
9,5% |
| sauts cond |
9,3% |
17,5% |
12,0% |
0,6% |
11,5% |
4,0% |
0,6% |
17,5% |
8,0% |
| sauts incond |
1,6% |
7,1% |
3,0% |
0,0% |
1,5% |
0,0% |
0,0% |
7,1% |
1,5% |
| décalages |
0,5% |
3,8% |
2,0% |
0,0% |
1,9% |
1,0% |
0,0% |
3,8% |
1,5% |
| mult int |
0,0% |
1,4% |
0,0% |
0,0% |
2,3% |
1,0% |
0,0% |
2,3% |
0,5% |
| fadd/sous |
0,0% |
0,0% |
0,0% |
5,2% |
16,6% |
10,0% |
0,0% |
16,6% |
5,0% |
| fmul |
0,0% |
0,0% |
0,0% |
4,1% |
12,9% |
8,0% |
0,0% |
12,9% |
4,0% |
| fdiv |
0,0% |
0,0% |
0,0% |
0,0% |
0,6% |
0,0% |
0,0% |
0,6% |
0,0% |
| fmove |
0,0% |
0,0% |
0,0% |
0,7% |
2,6% |
1,0% |
0,0% |
2,6% |
0,5% |
Il faut remarquer que plus d'une instruction sur 3 est un accès à la mémoire (et jusqu'à une
instruction sur 2). Cela tient
au fait que MIPS est une architecture chargement/rangement .
Par ailleurs, une instruction sur 12 est un saut conditionnel (cela peut aller jusqu'à une
instruction sur 6).
Il y a peu de sauts inconditionnels. Ce sont surtout des appels (saut immédiat avec lien) et retours
(saut indirect).
Pour les calculs et les unités nécessaires, une instruction sur 4 utilise l'additionneur
entier (cela peut aller jusqu'à une sur 3).
Une instruction sur 10 est une opération logique (avec un pic à une instruction sur 5).
Les décalages sont peu nombreux, de même que les multiplications entières. Il n'y a pas de division entière et un tel opérateur est superflu (il est préférable d'émuler l'opération).
Dans les codes flottants, une instruction sur 10 utilise l'additionneur flottant avec un pic
à une instruction sur 6.
Une instruction sur 12 est
une multiplication flottante avec un maximum de une sur 8.
Les
divisions flottantes sont très minoritaires mais un opérateur est utile pour certains codes.
La figure ci-dessous représente l'oganisation générale d'un processeur.
Le compteur de programme cp pointe sur la hiérarchie mémoire
d'instructions hmi .
A chaque cycle, on extrait d instructions ( d est le degré superscalaire du processeur) d'un même bloc de base (sans saut)
ou de plusieurs blocs de base successifs.
Les instructions extraites sont mises en file d'attente pour leur exécution.
Au passage, elles sont organisées en ordre partiel de leurs dépendances
LAE (Lecture Après Ecriture).
A partir du compteur de programme, on calcule ou on prédit à chaque cycle le compteur
de programme suivant cpsv .
Une fois les instructions lues de la mémoire, un examen permet d'en repérer
les sauts inconditionnels immédiats, ce qui implique un éventuel ajustement
du compteur de programme s'il a été incorrectement prédit.
A chaque cycle, parmi les instructions dont les sources sont disponibles (les instructions
prêtes ), on en lance au plus une par unité de calcul (par exemple,
une opération pour l'UAL, une addition flottante, un saut conditionnel
et un chargement).
Chaque unité de calcul dispose de ses ports de lecture et d'écriture dans le banc de registres.
Les opérateurs des unités de calcul sont pipelinés, ce qui permet de lancer une
nouvelle opération à chaque cycle. Le diviseur n'est pas pipeliné, ce qui impose
une latence d'un cycle par paire de bits de la mantisse à produire. La
hiérarchie mémoire de donnée est pipelinée: l'accès au cache est non bloquant.
Le résultat est écrit dans le banc de registres, les sources dépendantes
deviennent disponibles et l'instruction ayant calculé le résultat est
terminée.
La direction des sauts conditionnels et la cible des sauts indirects sont
confrontées aux prédictions. En cas de différence, un nouveau cpsv
est envoyé à l'unité d'extraction et la file de réordonnancement est vidée.
Une fois terminées, les instructions sortent de la file de réordonnancement dans
l'ordre de leur extraction. Les ordres d'écriture en mémoire sont effectués.