Simulation du microprocesseur GAPP en Smalltalk

(suite)

La classe Gapp

La classe Gapp représente le microprocesseur complet, la classe Pe les Processeurs élémentaires.

Chaque Gapp devra connaître ses 72 Pe, et les Pe doivent être organisés en une grille.

Les tableaux de Smalltalk sont monodimensionnels mais les cellules des tableaux peuvent pointer vers n’importe quel type d’objet. Pour construire une grille à deux dimensions, nous pouvons simplement construire un tableau de tableaux.


 

Chaque Gapp aura une variable grille pointant vers un tableau de colonnes. Les tableaux représentant les colonnes auront les Pe individuels comme élément. Dans notre cas, on construit un Gapp de 6 colonnes, et chaque colonne possède 12 éléments.

 

Les Pe sont reliés entre eux par les liens nord sud est et ouest en respectant le même schéma. De plus, au moment de la construction, chaque Pe est informé de sa position (x@y) et la conserve.

 

Au niveau d’un Gapp, on accède à un Pe de coordonnées (x@y) par la formule :

(grille at: x) at: y
où x et y varient respectivement de 1 à 6 et de 1 à 12.

 

 


 

Object subclass: #Gapp

            instanceVariableNames: 'go memoire grille programme '

            classVariableNames: ''

            poolDictionaries: ''

            category: 'VL-Gapp'!

 

Définition de la classe Gapp. Elle fait apparaître trois autres variables, go sera la sortie globale, memoire pointera vers notre représentation de la mémoire des Pe, et programme sera une liste d’instructions à exécuter.

!Gapp methodsFor: 'initialize-release'!

 

initialize

            | taille |

            taille :=  (6@12).

            memoire := Ram new: taille profondeur: 128.

            grille := (1 to: taille x) collect: [:x |

                        (1 to: taille y) collect: [:y |

                                   (Pe new: (x @ y)) gapp: self ]].

            self connectionTorique.

            programme := OrderedCollection new.! !

La méthode d’initialisation de la classe Gapp indique comment construire un Gapp. C’est la première méthode qui est appelée quand on crée un nouveau Gapp avec l’expression Gapp new.

Une variable temporaire indique la taille de la grille sous la forme d’un point (une instance de la classe Point).

La ligne suivante construit la représentation de la mémoire.

La grille est construite en collectionnant des collections de nouveaux Pe. Les Pe sont créés avec l’expression (Pe new: (x@y)). Puis, le message gapp: self permet d’indiquer au Pe quel est le Gapp qui le contient (chaque Pe pourra accéder à sa mémoire en passant par son lien gapp).

 


 

Dans notre modèle, la mémoire de chaque Pe n’est pas localisée directement dans le Pe comme dans le modèle original. Pour simplifier nous avons utilisé des images à un bit par pixel pour représenter les plans de mémoire. Comme chaque Pe doit disposer de 128 bits de RAM, nous construisons une table de 128 images (désignée par la variable frames). La profondeur 128 est également une variable, pour le cas où l’on souhaiterait changer la capacité mémoire des Pe.

 

Chaque Pe connaît le gapp et sa position. La valeur de l’adresse servira d’index dans la table frames pour sélectionner la bonne image et la position du Pe servira d’index dans l’image. Les images répondent à un message atPoint: pour délivrer la valeur de l’image au point donné en argument.

 


 

Pour revenir à la construction de la grille des Pe, une explication du message collect: s’impose.

Premier exemple :

(1 to: 10) collect: [:x | x ] → #(1 2 3 4 5 6 7 8 9 10)

(1 to: 10) représente la série des nombres de 1 à 10. C’est une instance de la classe Interval.

[ :x | x ] est un bloc. Les blocs permettent de mémoriser des instructions. Dans ce cas, le bloc délivre la valeur qu’on lui donne. Dans l’exemple ci-dessous, le bloc délivre le double de ce qu’on lui donne.

(1 to: 10) collect: [:x | x * 2 ] → #(2 4 6 8 10 12 14 16 18 20)

Enfin, le message collect: construit la collection des résultats d’évaluation du bloc, pour chacun des éléments de la collection qui reçoit le message.

Par exemple,

(1 to: 10) collect: [ :i | 'a' ] → #('a' 'a' 'a' 'a' 'a' 'a' 'a' 'a' 'a' 'a' )

ou encore :

(3 to: 7) collect: [ :valeur | 1 + valeur * valeur  + 1]  →  #(13 21 31 43 57)

ou :

#(1 1.0 't' z () $a 3r12 0.0d) collect: [ :o | o class ]   #(SmallInteger Float ByteString ByteSymbol Array Character SmallInteger Double)

 

Revenons à nos Pe, pour construire un tableau de tableaux, il suffit d’imbriquer deux messages collect:.

 

(1 to: 3) collect: [ :a | (7 to: 10) collect: [ :b | b] ]

 

a prend successivement les valeurs 1, 2 et 3. Pour chaque valeur, on refait un collect: qui construira un tableau avec les valeurs successives de b (de 7 à 10).

Le résultat est donc : #(#(7 8 9 10) #(7 8 9 10) #(7 8 9 10))

 

Là, le paramètre du premier bloc, a, n’est pas utilisé, mais il est connu dans le second bloc.

Construisons maintenant un Point dans le second bloc avec a pour x et b pour y.

 

(1 to: 3) collect: [ :a | (7 to: 10) collect: [ :b | a@b] ]

 

Cela donne : #(#(1@7 1@8 1@9 1@10) #(2@7 2@8 2@9 2@10) #(3@7 3@8 3@9 3@10))

Maintenant revenons à l’expression qui construisait les Pe dans la méthode d’initialisation :

 

grille := (1 to: taille x) collect: [:x |

                        (1 to: taille y) collect: [:y |

                                   (Pe new: (x @ y)) gapp: self ]].

Les deux boucles construisent un tableau de tableaux de Pe, avec les coordonnées transmises à chaque Pe.

 

 

!Gapp methodsFor: 'accessing'!

 

memoire

            ^ memoire!

 

programme

            ^programme! !

 

!Gapp methodsFor: 'connexions'!

Méthodes d’accès pour le Gapp.

Si on connaît un Gapp, on peut lui demander sa mémoire ou son programme. Ceci est nécessaire pour construire l’interface. La méthode mémoire est utile aux Pe pour accéder à la mémoire.

 

 

connectionTorique

            | taille pe |

            taille := memoire taille.

            1 to: taille x do: [:x |

                        1 to: taille y do: [:y |

                                   pe := ((grille at: x) at: y).

                                   pe sud: ((grille at: x) at: y \\ taille y + 1).

                                   pe est: ((grille at: x \\ taille x + 1) at: y) ]]! !

La méthode connectionTorique établit les liens nord sud est et ouest entre les Pe.

La taille de la grille est récupérée dans la mémoire (La Ram mémorise ou sait recalculer la taille, largeur@hauteur de la grille).

Le message to:do: est un peu comme le message collect: envoyé à un intervalle, mais il ne construit pas la collection de résultats, il exécute simplement le bloc pour chaque valeur. Les deux messages to:do: imbriqués permettent de parcourir l’ensemble de la grille de Pe a partir de leur coordonnées.

Le message \\ calcule le reste de la division entière, en fait une sorte de modulo, qui permet dans ce cas de brancher l’est du Pe le plus à l’est sur le Pe le plus à l’ouest (et inversement, et de même à 90 degrés pour l’axe nord-sud).

Pourquoi les +1 ?

Les index dans les tableaux varient de 1 à N. Alors que les modules varient de 0 à N-1. On veux ajouter 1, mais si on dépasse la limite il faut revenir à 1.

L’expression y \\ taille y + 1 est à paraphraser par : « y modulo hauteur plus 1 ».

 

y

taille y

y \\ taille y

y \\ taille y + 1

1

12

1

2

2

12

2

3

3

12

3

4

4

12

4

5

5

12

5

6

6

12

6

7

7

12

7

8

8

12

8

9

9

12

9

10

10

12

10

11

11

12

11

12

12

12

0

1

 

 

 

 

 

 

 

 

 


Notez que l’on ne fait le lien, apparemment que vers l’est et le sud. En fait, comme les liens sont symétriques, le fait d’indiquer à un Pe que son voisin d’est est untel, implique que l’ouest de untel soit ce Pe (voir les méthodes est: et sud: dans la classe Pe).

 

!Gapp methodsFor: 'execution'!

 

cycle1

            grille do: [:colonne |

                        colonne do: [:pe |

                                   pe cycle1 ]].!

 

cycle2cm: codecm ns: codens ew: codeew c: codec ram: coderam

            grille do: [:colonne |

                        colonne do: [:pe |

                                   pe cycle2cm: codecm ns: codens ew: codeew c: codec ram: coderam ]]!

 

cycle3

            go := 0.

            grille do: [:colonne |

                        colonne do: [:pe |

                                   pe cycle3.

                                   go := go bitOr: (pe ns) ]]!

 

executer

            programme do: [:instruction |

                        memoire adresse: instruction adresse.

                        instruction executeAvec: self ]! !

Protocole regroupant les méthodes concernant l’exécution pour le Gapp.

 

La méthode executer (sans accent dans le code), est très simple à comprendre.

Le programme est une liste d’instructions. L’exécution consiste à parcourir cette liste (avec le message do:) et à traiter chaque instruction.

On indique à la mémoire Ram quelle est l’adresse qui est dans l’instruction. Ceci représente le travail du décodeur d’adresse dans notre modèle. Puis on invoque la méthode nommée « executeAvec: » de l’instruction en passant le Gapp en argument. L’instruction va à son tour appeler les méthodes « cycle1 », « cycle2cm:ns:ew:c:ram:» et « cycle3 » du processeur.

Le cycle 1 prépare l’exécution en sauvant les valeurs que les Pe pourront transmettre ou utiliser ensuite.

Le cycle 2 traite les 5 portions du code opératoire en exécutant le « microcode » correspondant.

Le cycle 3 fait fonctionner les Ual, puis collecte les valeurs de registre NS pour calculer la valeur de la sortie globale go.

Le cycle 1 est nécessaire car nous simulons un processeur parallèle avec un programme qui s’exécute séquentiellement. Les cycles au niveau de la classe Gapp consistent principalement à retransmettre le message à chaque Pe. Mais il ne faut pas par exemple que l’on modifie dans un Pe une valeur qui doit être utilisée par un Pe voisin (Ce ne serait pas cool du tout).

Quand on exécute l’instruction  EW = W par exemple, chaque Pe doit affecter son registre EW avec la valeur du registre EW du Pe situé à l’ouest. Si on effectuait cette opération directement dans les variables ew qui représentent les registres EW et successivement d’ouest en est,  la valeur du Pe le plus à l’est serait recopiée dans tout les autres. Pour éviter cela, le cycle 1 des Pe sauvegarde la valeur de ew dans outputEw. Pour simplifier encore, les variables inputE et inputW sont affectées respectivement avec les valeurs des registres EW des Pe est et ouest.

Habituellement, pour simuler du parallélisme, on crée deux représentations de l’objet, une pour le temps t et une pour le temps t+1. On calcule les valeurs de t+1 à partir des valeurs de t, puis on permute l’usage des représentations, en attribuant les rôles inverses : la représentation t+1 devient celle du temps présent, et celle de l’ancien temps t, devenue inutile, est promue pour représenter le temps futur.

Le dernier cadre contient les méthodes de la métaclasse Gapp class.

Il consiste juste à spécifier l’initialisation des nouveaux objets. Gapp new !

 

"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!

 

Gapp class

            instanceVariableNames: ''!

 

 

!Gapp class methodsFor: 'instance creation'!

 

new

            ^ super new initialize! !

 

(à suivre…)

Sommaire de la suite :

La classe Ram (Qui représentera la mémoire locale des Pe).

La classe Instruction (qui permettra de programmer nos Microprocesseurs)

La classe Pe (Qui représente les Processeurs élémentaires)

L’interface et la classe GappSim, pour visualiser et interagir avec notre simulation.