Vous trouverez ici une brève description d'un modèle de machine de type ram et un simulateur qui vous permettra de tester vos propres programmes. Cliquez sur ce lien pour récupérer le mémento en pdf.
Description physique de la machine
La machine ram, acronyme de Random Access Machine (qu'il faut traduire par Machine à adressage direct) ou encore Register Adressable Memory, est un modèle abstrait de calcul introduit dans les années 60. Ce modèle avait pour objectif de fournir un outil d'étude des algorithmes plus proche des ordinateurs que ne l'étaient les modèles abstraits déjà proposés comme les fonctions récursives, le λ-calcul ou encore la machine de Turing. Même si l'écriture d'algorithmes sur la machine ram reste fastidieuse, son langage de programmation s'apparente aux langages d'assemblage beaucoup plus familiers des informaticiens.
Cette machine est constituée de 4 éléments principaux :
Son fonctionnement est rudimentaire. On charge au préalable les instructions du programme dans le bloc programme (ce chargement est bien entendu virtuel, il s'agit d'un modèle abstrait) et on saisit (toujours virtuellement) les données à traiter sur la bande d'entrée. Ces données sont encodées sous formes d'entiers bornés suivant un schéma d'encodage arbitraire. La borne pour ces entiers et le schéma d'encodage, comme celui de décodage, sont arbitraires. Les instructions \(P_k\) du programme sont décodées séquentiellement, pour cela, un registre particulier de la machine appelé compteur ordinal (co sur le schéma ci-dessus qui joue le rôle de l'indice \(k\)) contient précisément l'adresse du registre contenant l'instruction à décoder, initialement 0. Après qu'une instruction a été décodée, le compteur ordinal est incrémenté d'une unité pour passer à l'instruction suivante du programme sauf si l'instruction est une rupture de séquence, auquel cas son rôle est justement de modifier la valeur du compteur ordinal. La machine arrête le cycle de décodage dès que l'instruction STOP a été exécutée. Les données à traiter peuvent être chargées dans la mémoire à l'aide de l'instruction de lecture READ et les résultats des calculs sont renvoyés à l'utilisateur sur la bande de sortie à l'aide de l'instruction d'écriture WRITE.
NB. Dans cette architecture, dite de type Harvard, le stockage du programme se fait dans une zone séparée de celle qui sontient les données et ne peut donc être modifié lors de son exécution. Dans une architecture dite de type Von Neuman, où le code programme est placé dans la même mémoire que celle des données (au début en général), celui-ci peut donc être modifié par sa propre exécution.Jeu d'instructions
Les instructions peuvent être regroupées selon leur nature en 4 groupes distincts :
Pour la compréhension du jeu d'instruction et du fonctionnement de cette machine, nous décrivons symboliquement la bande d'entrée, la bande de sortie et la mémoire respectivement par les listes \(E\), \(S\) et \(R\) et l'écriture conventionnelle \(L[i]\) pour désigner le contenu d'une telle structure à la position \(i\).
Le registre \(R_0\) joue un rôle central dans le fonctionnement de la machine ram, il est appelé accumulateur et est également noté acc :
Les lectures et écritures étant séquentielles, cela signifie qu'un fois une valeur lue sur la bande d'entrée avec l'instruction READ (resp. WRITE), la prochaine lecture (resp. écriture) se fera sur la cellule suivante sans retour possible. Dans les tables ci-dessous l'interprétation d'une instruction est écrite sous une forme algorithmique très commune juste à côté.
Entrées-sorties
READ | ACC ← E[i++] | Lecture : copie une valeur de la bande d'entrée dans l'accumulateur. |
WRITE | S[i++] ← ACC | Écriture : copie le contenu de l'accumulateur sur la bande de sortie. |
Gestion mémoire, adressage
- Chargement -
LOAD #n | ACC ← n | Absolu : charge la valeur n dans l'accumulateur. |
LOAD n | ACC ← R[n] | Direct : copie le contenu du registre d'adresse \(n\) dans l'accumulateur. |
LOAD @n | ACC ← R[R[n]] | Indirect : copie le contenu du registre d'adresse \(R[n]\) dans l'accumulateur. |
- Stockage -
STORE n | R[n] ← ACC | Direct : copie le contenu de l'accumulateur dans le registre d'adresse \(n\). |
STORE @n | R[R[n]] ← ACC | Indirect : copie le contenu de l'accumulateur dans le registre d'adresse \(R[n]\). |
Opérations arithmétiques
- Décrément / Incrément -
DEC n | R[n] ← R[n] - 1 | Direct : décrémente le contenu du registre d'adresse \(n\). |
DEC @n | R[n] ← R[R[n]] - 1 | Indirect : décrémente le contenu du registre d'adresse \(R[n]\). |
INC n | R[n] ← R[n] + 1 | Direct : incrémente le contenu du registre d'adresse \(n\). |
INC @n | R[n] ← R[R[n]] + 1 | Indirect : incrémente le contenu du registre d'adresse \(R[n]\). |
- Opérations arithmétiques-
ADD #n | ACC ← ACC + n | Absolu : ajoute la valeur \(n\) à l'accumulateur. |
ADD n | ACC ← ACC + R[n] | Direct : ajoute le contenu du registre d'adresse \(n\) à l'accumulateur. |
ADD @n | ACC ← ACC + R[R[n]] | Indirect : ajoute le contenu du registre d'adresse \(R[n]\) à l'accumulateur. |
SUB #n | ACC ← ACC + n | Absolu : soustrait la valeur \(n\) à l'accumulateur. |
SUB n | ACC ← ACC + R[n] | Direct : soustrait le contenu du registre d'adresse \(n\) à l'accumulateur. |
SUB @n | ACC ← ACC + R[R[n]] | Indirect : soustrait le contenu du registre d'adresse \(R[n]\) à l'accumulateur. |
MUL #n | ACC ← ACC + n | Absolu : multiplie la valeur \(n\) à l'accumulateur. |
MUL n | ACC ← ACC + R[n] | Direct : multiplie le contenu du registre d'adresse \(n\) à l'accumulateur. |
MUL @n | ACC ← ACC + R[R[n]] | Indirect : multiplie le contenu du registre d'adresse \(R[n]\) à l'accumulateur. |
DIV #n | ACC ← ACC + n | Absolu : quotiente l'accumulateur par la valeur \(n\). |
DIV n | ACC ← ACC + R[n] | Direct : quotiente l'accumulateur par le contenu du registre d'adresse \(n\). |
DIV @n | ACC ← ACC + R[R[n]] | Indirect : quotiente l'accumulateur par le contenu du registre d'adresse \(R[n]\). |
MOD #n | ACC ← ACC + n | Absolu : réduit l'accumulateur modulo la valeur \(n.\) |
MOD n | ACC ← ACC + R[n] | Direct : réduit l'accumulateur modulo le contenu du registre d'adresse \(n\). |
MOD @n | ACC ← ACC + R[R[n]] | Indirect : réduit l'accumulateur modulo le contenu du registre d'adresse \(R[n]\). |
Ruptures de séquence
- Inconditionnelles -
JUMP n | CO ← n | Direct : saut à l'instruction \(n\). |
JUMP @n | CO ← R[n] | Indirect : saut à l'instruction \(R[n]\). |
STOP | Arrêt du programme. | |
NOP | Instruction nulle. Ne fait rien. |
- Conditionnelles -
JUMZ n | IF (ACC = 0) CO ← n | Direct : saut à l'instruction \(n\) si la valeur de l'accumulateur est \(0\). |
JUMZ @n | IF (ACC = 0) CO ← R[n] | Indirect : saut à l'instruction \(R[n]\) si la valeur de l'accumulateur est \(0\). |
JUML n | IF (ACC < 0) CO ← n | Direct : saut à l'instruction \(n\) si la valeur de l'accumulateur est strictement inférieure à \(0\). |
JUML @n | IF (ACC < 0) CO ← R[n] | Indirect : saut à l'instruction \(R[n]\) si la valeur de l'accumulateur est strictement inférieure à \(0\). |
JUMG n | IF (ACC > 0) CO ← n | Direct : saut à l'instruction \(n\) si la valeur de l'accumulateur est strictement supérieure à \(0\). |
JUMG @n | IF (ACC > 0) CO ← R[n] | Indirect : saut à l'instruction \(R[n]\) si la valeur de l'accumulateur est strictement supérieure à \(0\). |
L'instruction NOP qui ne fait rien (no operation) permet, par exemple, en l'intercalant entre certaines instructions du code, de recaler correctement les instructions de saut en cas de modification du code sans avoir à changer les adresses de saut.
Modèle abstrait
La description quasi physique de cette machine ne doit pas masquer le fait qu'il s'agit bien d'un modèle abstrait, on peut parfaitement le définir dans le langage de la théorie des ensembles. Dans ce cadre abstrait, une machine est identifiée et caractérisée par son programme, à savoir la séquence d'instructions qui détermine son comportement pour une entrée donnée. Ici nous considérons le modèle borné, c'est-à-dire celui dans lequel les entiers manipulés sont bornés. Comme il y a \(36\) opérations distinctes au total ‐ en intégrant les différents types d'adressage pour les instructions concernées, absolu et indirect ‐ et elles opérent sur des entiers relatifs pour plus de souplesse. Un programme est donc une séquence de couples \((op,x)\) de \(\ab{0}{35}\times{\mathbf Z}\). On peut à présent résumer cette machine ram à la définition suivante :
On peut également décrire le fonctionnement d'une machine ram à travers ses configurations. Une configuration est un sexuplet \((E,i,S,j,co,R)\) où \(i\), \(j\) et \(co\) sont des entiers naturels, \(E\), \(S\) et \(R\) sont des suites à valeur dans un intervalle \([a,b]\) de cardinal \(B\). La configuration initiale de la machine est fixée à \(C_0:=(x,0,0^\infty,0,0^\infty)\) où \(x\) désigne la suite codant les données à traiter et \(0^\infty\) la suite identiquement nulle. On peut alors définir une fonction de transition \(\delta:\mathcal C\rightarrow\mathcal C\) de l'ensemble des configurations dans lui-même et qui décrit comment l'on passe d'une configuration à la suivante après le décodage d'une instruction. L'exécution du programme consiste à itérer la fonction \(\delta\) pour obtenir la suite des configurations \(C_n:=\delta^n(C_0)\).
Cette suite est convergente si l'algorithme s'arrête, autrement dit cette suite est constante à partir d'un certain rang. Si \(e=e_0,e_1,\ldots,e_{n-1}\) désigne une entrée et \(P\) un programme de la machine ram qui s'arrête pour l'entrée \(e\), on note ce rang \begin{equation} \eta(e)=\min\{N\in{\mathbf N}\mid \forall n > N,\ C_n=C_N\} \end{equation} Il s'agit tout simplement du nombre total d'instructions décodées par la machine ram pour traiter l'entrée \(e\) et s'arrêter. Ce modèle nous permet à présent de définir précisément ce qu'est un algorithme.
Exemples
Ce premier algorithme que vous pouvez charger sur le simulateur, calcule la partie entière de la moyenne arithmétique des valeurs en entrée. Par convention la valeur nulle sert de marqueur pour la fin des données à lire sur la bande d'entrée. Le registre \(R_1\) contient le nombre de valeurs non-nulles lues et le registre \(R_2\) la somme des valeurs non-nulles lues.
LOAD #0 ; ACC ← 0 STORE 1 ; R[1] ← ACC STORE 2 ; R[2] ← ACC READ ; ACC ← ENTREE[i++] JUMZ 9 ; SI ACC = 0 SAUTER A L'INSTRUCTION #9 ADD 2 ; ACC ← ACC + R[2] STORE 2 ; R[2] ← ACC INC 1 ; R[1] ← R[1] + 1 JUMP 3 ; SAUTER A L'INSTRUCTION #3 LOAD 2 ; ACC ← R[2] DIV 1 ; ACC ← ACC / R[1] WRITE ; SORTIE[j++] ← ACC STOP ; ARRETLes instructions #0, #1 et #2 initialisent l'accumulateur et les registres \(R_1\) et \(R_2\) à \(0\). Les instructions #3 à #8 constituent la boucle de lecture et additionnent les valeurs non-nulles de la bande d'entrée dans le registre \(R_2\).
L'algorithme suivant cherche la plus grande valeur parmi celles qui sont fournies en entrée. Par convention la valeur nulle sert de marqueur pour la fin des données à lire sur la bande d'entrée. Le registre \(R_1\) sert à mémoriser la plus grande valeur courante et le registre \(R_2\) permet de conserver la dernière valeur lue. On suppose que la liste contient au moins une valeur, il y a donc au moins deux cellules de la bande d'entrée qui sont utilisées.
READ ; ACC ← ENTREE[i++] STORE 1 ; R[1] ← ACC READ ; ACC ← ENTREE[i++] JUMZ 11 ; SI ACC = 0 SAUTER A L'INSTRUCTION #11 STORE 2 ; R[2] ← ACC LOAD 1 ; ACC ← R[1] SUB 2 ; ACC ← ACC - R[2] JUMG 2 ; SI (R[2] - R[1] > 0) SAUTER A L'INSTRUCTION #2 LOAD 2 ; ACC ← R[2] STORE 1 ; R[1] ← ACC JUMP 2 ; SAUTER A L'INSTRUCTION #2 LOAD 1 ; ACC ← R[1] WRITE ; SORTIE[j++] ← ACC STOP ; ARRET
Ce troisième algorithme prend en entrée une liste de valeurs et fait remonter la plus grande à la fin de la liste. La valeur 0 sert encore une fois de balise de fin de lecture. Le principe général est simple, on compare les valeurs contiguës deux-à-deux dans l'ordre de lecture de la liste en les permutant si nécessaire, ainsi à la fin du parcours la plus grande valeur est en fin de liste. Par exemple, si la bande d'entrée contient les valeurs \(3,1,6,2,5,1,0\), la bande de sortie contiendra la séquence \(1,3,2,5,1,6\). Il s'agit du principe sous-jacent du tri à bulles.
Dans un premier temps, l'algorithme lit les toutes les valeurs de la bande d'entrée et range les \(n\) valeurs non-nulles dans des registres consécutifs de la mémoire à partir du registre \(R_5.\) Dans un deuxième temps, l'algorithme fait les \(n-1\) comparaisons \(R_i\leq R_{i+1}\) pour \(i\in[5,n+3]\) et permute le contenu des registres si nécessaire. Dans un troisième et dernier temps, l'algorithme écrit les valeurs contenus dans les registres mémoire \(R_5\) à \(R_{n+4}\) sur la bande de sortie.
Les registres \(R_0\) (accumulateur) et \(R_1\) serviront pour les calculs et les registres 2 à 4 seront utilisés pour:
Phase de lecture : les instructions #0 à #3 initialisent les registres \(R_2\leftarrow 0\) et \(R_4\leftarrow 5\). Les instructions #2 à #9 constituent la boucle de lecture des valeurs de la bande d'entrée, le registre \(R_2\) est incrémenté pour déterminer combien de valeurs non-nulles auront été lues. L'instruction #5 teste si la valeur lue est nulle et détourne le programme à l'instruction #10 qui débute la phase 2.
Phase d'écriture : les instructions #31 à #35 réinitialisent les registres \(R_2\leftarrow n\) et \(R_4\leftarrow 5\). Les instructions #36 à #42 constituent la boucle d'écriture de la mémoire vers la bande de sortie.
Phase de calcul : l'instruction #16 teste s'il y a encore des comparaisons à faire entre les registres \(R_{i}\) et \(R_{i+1}\). Dans ce cas, on soustrait les valeurs des deux registres et l'instruction #19 teste s'il y a lieu de faire la permutation (rôle des instructions #21 à #26). Sinon on passe à la phase d'écriture.
LOAD #0 ; ACC ← 0 STORE 2 ; R[2] ← ACC LOAD #5 ; ACC ← 5 STORE 4 ; R[4] ← ACC READ ; ACC ← ENTREE[i++] JUMZ 10 ; SI ACC = 0 SAUTER A L'INSTRUCTION #10 STORE @4 ; R[R[4]] ← ACC INC 4 ; R[4] ← R[4] + 1 INC 2 ; R[2] ← R[2] + 1 JUMP 4 ; SAUTER A L'INSTRUCTION #4 DEC 2 ; R[2] ← R[2] - 1 LOAD #5 ; ACC ← 5 STORE 3 ; R[3] ← ACC STORE 4 ; R[4] ← ACC INC 4 ; R[4] ← R[4] + 1 LOAD 2 ; ACC ← R[2] JUMZ 31 ; SI ACC = 0 SAUTER A L'INSTRUCTION #31 LOAD @4 ; ACC ← R[R[4]] SUB @3 ; ACC ← ACC - R[R[3]] JUML 21 ; SI ACC < 0 SAUTER A L'INSTRUCTION #21 JUMP 27 ; SAUTER A L'INSTRUCTION #27 LOAD @4 ; ACC ← R[R[4]] STORE 1 ; R[1] ← ACC LOAD @3 ; ACC ← R[R[3]] STORE @4 ; R[R[4]] ← ACC LOAD 1 ; ACC ← R[1] STORE @3 ; R[R[3]] ← ACC INC 3 ; R[3] ← R[3] + 1 INC 4 ; R[4] ← R[4] + 1 DEC 2 ; R[2] ← R[2] - 1 JUMP 15 ; SAUTER A L'INSTRUCTION #15 LOAD 4 ; ACC ← R[4] SUB #5 ; ACC ← ACC - 5 STORE 2 ; R[2] ← ACC LOAD #5 ; ACC ← 5 STORE 4 ; R[4] ← ACC LOAD 2 ; ACC ← R[2] JUMZ 43 ; SI ACC = 0 SAUTER A L'INSTRUCTION #43 LOAD @4 ; ACC ← R[R[4]] WRITE ; S[j++] ← ACC INC 4 ; R[4] ← R[4] + 1 DEC 2 ; R[2] ← R[2] - 1 JUMP 36 ; SAUTER A L'INSTRUCTION #36 STOP ; ARRET
Travaux pratiques
Le simulateur javascript vous permet d'étudier le comportement de la machine avec vos programmes. L'intervalle des valeurs autorisées est fixé à \([-16383,16384]\). Vous écrirez vos programmes dans un fichier texte avec votre éditeur préféré en codant une instruction par ligne. Tout ce qui suit le symbole ; est ignoré, donc commentez impérativement vos programmes !
Modifiez l'algorithme précédent de manière à afficher également le nombre de termes qui auront été additionnés (à l'exception de la première valeur nulle rencontrée lors de la lecture de la bande d'entrée).