12 KiB
Cours de Florent.
Calculer en théorie et en pratique III/III
Dans cette séquence de cours, nous tentons de répondre à la question suivante.
C'est quoi un ordinateur?
Cette question s'apparente à celle que ce sont posés philosophes et mathématiciens de Leibnitz à Turing en passant par Hilbert.
Qu'est-ce qu'une question à laquelle on peut répondre de manière mécanique par un calcul?
Nous avons vu une approximation de calcul théorique basé sur le calcul booléen et des portes logiques qui sont implémentable avec des composants electroniques, permettant de réaliser divers opérations comme l'addition bit à bit (schéma de Vernam en cryptographie), la majorité ou encore l'addition avec retenue.
Nous avons poursuivi avec un modèle théorique, qui est celui de l'automate fini, un modèle assez restreint de machine : l'adjectif fini s'applique à la mémoire de la machine qui est finie. Nous avons entrevu le modèle plus général avec une mémoire arbitrairement grande la machine de Turing et évoqué la thèse de Church-Turing qui stipule que tout modèle de calcul raisonnable est équivalent à ce modèle de la machine de Turing.
Toutefois, le modèle de la machine de Turing reste assez éloigné d'une machine réelle et nous allons maintenant présenter rapidement un modèle simplifié de machine basé sur le modèle de Von Neumann, puis pratiquer l'assembleur avec la Machine Virtuelle à Pile (MVàP).
Programme vs données
Dans le modèle de Turing, le programme est externe et guide la machine qui travaille sur le contenu du ruban qu'on peut voir comme la donnée. Dès 1936, Turing montre qu'en fait on peut construire une machine universelle qui lit sur son ruban le programme et les données sur lesquelles il faut lancer le calcul de ce programme.
C'est cette idée de "programme qu'on va charger" qu'on retrouve dans le modèle de Von Neumann.
Le modèle de Von Neumann
- L'unité de Mémoire contient des données et des insructions
- Le processeur se compose d'une unité de contrôle et d'une unité arithmétique et logique.
- La communication avec l'extérieur se fait par des dispositifs d'entrée-sortie.
Pour plus de détails, voir wikipedia
Zoom sur l'unité arithmétique et logique
Essentiellement un circuit au sens de ce qu'on a évoqué lors du dernier cours. En pratique les entrées/sorties sont découpées en 2 dimensions : une partie contrôle et une partie données.
Zoom sur la mémoire
La mémoire peut être vue comme une pyramide.
- En haut très peu de place, mais de la mémoire très rapide, ce sont des registres.
- Au milieu, de la mémoire assez rapide, c'est la mémoire vive (la RAM) qui n'est pas persistante et quand on étend la machine, le contenu de la mémoire n'est pas conservé.
- En bas, de la mémoire plutôt lente, mais persistante, typiquement le disque dur.
En pratique il existe des mécanismes de cache, en particulier entre les registres et la RAM.
Il y a d'autres ingrédients qui permettent de limiter l'emprunte en mémoire d'un programme (adresses virtuelles, paging) voir de suspendre une exécution et de recopier le programme en train de calculer dans le disque (swapping).
la Machine Virtuelle à Pile (MVàP).
Normalement dans une machine il faudrait expliquer comment les calculs sont poussés vers les registres et l'ALU. Nous allons travailler avec un modèle simplifié qui cache cet aspect et dans laquelle la mémoire est une pile.
Par ailleurs la machine possèdes deux registres.
- PC (pour Program counter) qui contient le numéro de la ligne d'instruction en cours
- FC (pour frame pointer) qui est un mécanisme permettant de faire des appels de fonctions mais qu'on n'utilisera pas car c'est un peu plus technique à utiliser.
La machine accepte plusieurs instructions.
PUSHI n # pousse l'entier n sur la pile et passe à l'instruction suivante
POP # décale le sommet de la pile d'un et passe à l'instruction suivante
ADD # ajoute les 2 entiers au sommet de la pile, décale le sommet de la pile de 2
# écrit le résultat en haut de la pile et passe à l'instruction suivante
SUB # même chose pour la soustraction
MUL # même chose pour la multiplication
DIV # même chose pour la division
READ # lecture d'un entier qui est poussé en haut de la pile
WRITE # écriture de l'entier en haut de la pile
HALT # fin du programme
LABEL x # définition d'une ancre vers laquelle on peut sauter
JUMP x # Saut vers le label (prochaine instruction à la ligne du label)
JUMPF x # Instruction suivante si le haut de la pile est différent de 0
# Instruction x sinon.
INF # Mange deux entiers.
# Remplace par l'entier 1 si le premier inférieur au second et par 0 sinon.
# Passe à l'instruction suivante
INFEQ # idem plus petit ou égal
SUP # idem plus grand
SUPEQ # idem plus grand ou égal
EQUAL # idem égal
NEQ # idem différent
# Autres instructions qu'on verra plus tard.
PUSHG a # empile en haut de la pile l'entier stocké à l'adresse a de la pile
STOREG a # dépile et stocke à l'adresse a de la pile l'entier en haut de la pile
# Autres instructions qu'on ne verra probablement pas plus tard.
CALL x # Appel du code stocké au label x
RETURN # Retour dans le code appellant.
PUSHL a # analogue de PUSHG pour des variables locales (utilise FP)
STOREL a # analogue de STOREG
L'ordre des opérations est naturel, si on voit la pile de côté, bas à gauche, haut à droite. Une opération qui mange deux arguments prendra l'avant dernier argument comme premier argument et le dernier comme second argument.
Un premier exemple
PUSHI 11
PUSHI 6
PUSHI 15
MUL
SUB
PUSHI 5
ADD
PUSHI 12
ADD
PUSHI 9
PUSHI 4
MUL
PUSHI 7
MUL
ADD
WRITE
POP
HALT
Une fois assemblé, le code devient :
Adr | Instruction
-----+---------------
0 | PUSHI 11
2 | PUSHI 6
4 | PUSHI 15
6 | MUL
7 | SUB
8 | PUSHI 5
10 | ADD
11 | PUSHI 12
13 | ADD
14 | PUSHI 9
16 | PUSHI 4
18 | MUL
19 | PUSHI 7
21 | MUL
22 | ADD
23 | WRITE
24 | POP
25 | HALT
La MVàP, en mode debug, écrit à chaque pas :
- la valeur du compteur de programme (pc) ;
- l'instruction à cette adresse ;
- la valeur du « frame pointer » (toujours 0, quand il n'y a pas d'appel de fonction) ;
- le contenu de la pile ;
- la hauteur de la pile.
Le début de l'exécution donne la trace suivante :
pc | | fp pile
====================================================
0 | PUSHI 11 | 0 [ ] 0 # On empile 11
2 | PUSHI 6 | 0 [ 11 ] 1 # On empile 6
4 | PUSHI 15 | 0 [ 11 6 ] 2 # On empile 15
6 | MUL | 0 [ 11 6 15 ] 3 # On dépile et multiplie 6*15, on empile le résultat
7 | SUB | 0 [ 11 90 ] 2 # On dépile et soustrait 90 à 11, on empile le résultat.
8 | PUSHI 5 | 0 [ -79 ] 1 # On empile 5.
etc
Exercice
Continuez l'exécution.
Exercice
Vérifier votre calcul manuel en lançant la machine.
Vous devez commencer par cloner le répo de ce cours.
Puis soit utiliser java, soit utiliser javascript. Le plus simples est la seconde solution.
Avec java
Il faut avoir java installé sur votre machine ou utiliser proxmox. Il faut utiliser les deux fichiers jar qui sont dans le répertoire MVaP Le fichier ```MVaP/ALIRE``` contient la marche à suivre.Avec Javascript : naviguer dans le répertoire intitulé MVàP puis MVaP-1.0. Trouvez le fichier index.html. ouvrez le acvec votre navigateur.
Variables globales
On peut réserver de l'espace en bas de la pile et associer à une variable sa position pour mémoriser des valeurs.
PUSHI 1 # Il faut initialement pousser une valeur ( par exemple 1)
... # Du code qui génère des choses dans la pile et une valeur en haut de la pile, par exemple 42
STOREG 0 # stocke 42 à l'adresse 0 de la pile (le 1 est remplacé par 42)
... # Du code qui fait des choses et qui tout à coup a besoin de connaître la valeur stockée
PUSHG 0 # empile 42 en haut de la pile.
Exemple
Imaginons qu'on souhaite faire du code qui :
- déclare une variable i
- lui affecte la valeur 6
- plus tard souhaite l'incrémenter.
PUSHI 1 # i habite à l'adresse 0
...
PUSHI 6
STOREG 0
...
PUSHI 1
PUSHG 0
ADD
STOREG 0
Boucles
Imaginons qu'on souhaite faire une boucle tant que
Tant que test :
quelque chose
La suite
Il faut utiliser des labels pour délimiter
- la partie test suivie de la décision de rentrer dans la boucle ou de sortir
- la partie interne à la boucle (quelque chose)
- la partie qui suit la boucle (La suite).
LABEL 0
# le test qui va laisser une valeur 0 ou 1 en haut de la pile
JUMPF 1 # saut conditionnel vers le label 1 si le test est Faux sinon continue
# quelque chose
JUMP 0
LABEL 1
#La suite
Exercice
Produire du code MVàP qui correspond au code python suivant.
j = 1
i = 0
i = j
while i < 10:
i += 1
print(i)
Testez votre code avec la MVàP.
Exercice
Produire du code MVàP qui s'inspire du code suivant et simule la lecture et le stockage d'un tableau avant de vider la pile. La version ci-dessous ne vide pas la pile.
# Boucle While
LABEL 19
# le test qui va laisser une valeur 0 ou 1 en haut de la pile
# PUSHI 0 # pour faux, donc je saute après
# PUSHI 1 # pour vrai, donc je ne saute pas.
READ # on demande à l'utilisateur
JUMPF 18 # saut conditionnel vers le label 1 si le test est Faux sinon continue
# quelque chose
READ
# Fin du quelque chose
JUMP 19
LABEL 18
#La suite
PUSHI 666
WRITE
POP
# Fin de la suite.
HALT
Exercice
Produire du code MVàP qui correspond au code python suivant.
# integer inputs (input returns a string that needs casting as an int)
m = int(input())
n = int(input())
if (m < n) :
m = n
else :
n = m
print m
print n
Réutiliser du code.
Si on a du code qu'on veut réutiliser plusieurs fois,
- on peut l'isoler entre un label et un jump
- on peut sauter vers le label
- malheureusement on ne peut pas sauter à la fin vers plusieurs endroits différents
- on pourrait faire des tests successifs avec des JUMP et des JUMPF pour sauter au bon endroit
- par contre on ne peut pas réutiliser le code un nombre arbitraire de fois.
Solution : on sauve dans un registre l'endroit dans lequel on doit retourner Le plus simple consiste à utiliser le haut de la pile.
Par contre il faut dans ce cas une instruction pour la MVàP qui permette d'écrire en haut de la pile la position actuelle du pc (c'est une partie du rôle de CALL) et il faut une instruction qui soit l'analogue de HALT pour un bloc et permette de retourner le pc à la valeur antérieure à l'appel (c'est une partie du rôle de RETURN).
Avec cette idée on peut faire un appel à une fonction qui ne prend pas de paramètre et ne retourne pas de valeur.
On peut communiquer avec le reste du programme via une variable globale.
Exemple
On peut ainsi par exemple faire une fonction qui mutliplie par 2 une variable globale. On appelle 3 fois la fonction et on affiche le résultat à chaque fois.
PUSHI 1 # variable globale i habite à l'adresse 0
JUMP 1
LABEL 0 # Notre Fonction
PUSHG 0
PUSHI 2
MUL
STOREG 0
RETURN # Fin de Notre Fonction
...
LABEL 1 # Main
CALL 0 # appel 1
PUSHG 0
WRITE
CALL 0 # appel 2
PUSHG 0
WRITE
CALL 0 # appel3
PUSHG 0
WRITE
Autres Exercices
Il faut pour chaque exercice produire et tester du code MVàP qui réaliser le calcul ou le programme demandé.
Exercice
Lire un entier n et calculez n4+56-2
Exercice
Lire un entier n et calculez 24+n6-2
Exercice
Même chose que ci-dessus mais avec un programme optimisé (qui donne le même résultat mais avec un minimum d'instructions).
Exercice
Lire un entier n. Si n est nul le programme écrit 20 sinon 10.
Exercice
Et parce qu'il faut bien une question un peu plus difficile. Lire un entier n. Le programme écrit 2^n.