wiki:MultiCourseTP5

Cours "Architecture des Systèmes Multi-Processeurs"

TP5 : Partage du bus dans les architectures multi-processeurs

(pirouz.bazargan-sabet@…)

A. Objectifs

On s'intéresse dans ce TP aux problèmes de performances posés par le partage du bus entre les processeurs. Quand on augmente le nombre de processeurs connectés sur le bus, celui-ci devient un goulot d'étranglement, car la « bande passante » du bus (nombre maximum d'octets transférés par unité de temps) est bornée. Le temps d'attente pour accéder au bus peut devenir très long lorsque le nombre de processeurs devient grand : la conséquence est que chaque processeur passe plus de temps bloqué en attente du bus que de temps à exécuter des instructions...

On utilise dans ce TP une architecture générique, définie dans les fichiers tp5_top.cpp et tp5.desc, où on peut faire varier le nombre de processeurs, grâce au paramètre NPROCS sur la ligne de commande. On en profite également pour introduire dans l'architecture différents périphériques qui seront utilisés dans ce TP et dans les TPs suivants.

B. Architecture matérielle

L'archive multi_tp5.tgz contient les fichiers dont vous aurez besoin. Créez un répertoire de travail tp5, et décompressez l'archive dans ce répertoire. Outre les fichiers tp5_top.cpp, et tp5.desc, vous trouverez comme d'habitude le logiciel applicatif dans le répertoire soft.

Chaque processeur possède son propre terminal TTY. On a donc plusieurs terminaux écran/clavier mais un seul contrôleur connecté au bus. Le segment mémoire associé au contrôleur TTY contient donc NPROCS*16 octets, puisque chaque terminal TTY possède 4 registres adressables.

En plus de la ROM de boot, de la RAM, et du contrôleur TTY, on utilise dans ce TP un contrôleur de terminal graphique (frame buffer controller), qui permet d'afficher des images sur un écran graphique.

Ce contrôleur permet de contrôler des écrans graphiques de dimension quelconque, et on utilisera dans ce TP un petit écran permettant d'afficher des images carrées de 256 lignes de 256 pixels. De plus on se limitera à des images en niveaux de gris, où chaque pixel est codé sur un octet (256 niveaux de gris).

Ce périphérique FBF contient un tampon mémoire accessible en lecture ou en ecriture. Dans le cas d'une image de 64 K pixels (256 lignes de 256 pixels), un premier tampon de 64 Koctets contient les valeurs de luminance des (256*256) pixels de l'image (la luminance du premier pixel de la première ligne est stocké sur un octet à l'adresse de base du segment, le premier pixel de la deuxiéme ligne est stocké à l'adresse de base + 256, etc.). Un second tampon de 64 K octets permet de stocker les valeurs de chrominance, mais il n'est pas utilisé dans le cas des images en niveaux de gris. Ces tampons mémoire sont parcourus périodiquement par un automate interne au contrôleur (à la fréquence video de 25 images par seconde), et l'image présente dans le tampon est donc affichée sur l'écran graphique toutes les 40 ms.

Question B1: En consultant l'en-tête du fichier pibus_frame_buffer.h, précisez la signification des arguments du constructeur du composant PibusFrameBuffer.

Question B2: quelle est la longueur du segment associé à ce composant ?

On utilisera dans ce TP des petits caches possédant une capacité de 16 lignes de 8 mots, avec un seul niveau d'associativité, soit une capacité de 512 octets pour le cache instructions et 512 octets pour le cache de données.

Générez le simulateur simul.x en utiisant la commande:

$ soclib-cc -p tp5.desc -t systemcass -o simul.x

C. Compilation de l'application logicielle

L'application logicielle proposée dans le fichier main.c consiste à construire et à afficher une image de 256 lignes de 256 pixels. Cette image est un échiquier de 64 cases noires et blanches. Chaque case est est un carré de 32 * 32 pixels.

On construit cette image ligne par ligne, en utilisant un tableau de 256 pixels (c'est à dire 256 octets), et on utilise l'appel système fb_sync_write() pour écrire une ligne complête de 256 pixels dans le frame buffer.

Question C1 : Pourquoi faut-il utiliser un appel système pour accéder (en lecture comme en écriture) au contrôleur de frame-buffer ? Que se passe-t-il si un programme utilisateur essaie de lire ou d'écrire directement dans le Frame Buffer, sans passer par un appel système ?

Question C2 : Pourquoi préfère-t-on construire une ligne complête dans un tableau intermédiaire de 256 pixels plutôt que d'écrire directement l'image - pixel par pixel - dans le frame buffer?

Question C3 : Consultez le fichier stdio.c, et expliquez la signification des trois arguments de l'appel système fb_sync_write(). Complétez le fichier main.c en conséquence.

Vérifiez que les adresses de base des différents segments définies dans le fichier seg_ld sont identiques à celles définies dans le fichier tp5_top.cpp décrivant l'architecture matérielle, et lancez la compilation du logiciel système et du logiciel applicatif dans le répertoire soft, en utilisant le makefile qui vous est fourni.

D. Caractérisation de l'application logicielle

L'architecture matérielle, et le protocole du PIBUS définissent 4 types de transactions sur le bus :

  1. IMISS : Lecture rafale d'une ligne de cache suite à un MISS sur cache d'instructions.
  2. DMISS : Lecture rafale d'une ligne de cache suite à un MISS sur cache de données.
  3. UNC : Lecture d'un mot appartenant à un segment non-cachable.
  4. WRITE : Ecriture d'un mot (vers la RAM, le contrôleur TTY ou le Frame Buffer)

On va commencer par caractériser l'application logicielle en l'exécutant sur une architecture monoprocesseur.

Pour cela, on va relever, lorsque l'application se termine. les informations accumulées dans différents compteurs d'instrumentation existant dans le contrôleur des cache de premier niveau.

Commencez par lancer la simulation sans limitation du nombre de cycles pour déterminer combien de cycles sont nécessaires pour afficher l'image. On considèrera que l'application est terminée quand on exécute l'appel système exit().

Relancez la simulation, en imposant le nombre de cycles à simuler, et en activant l'affichage des statistiques avec la commande (-STATS) sur la ligne de commande.

Question D1 : Quel est le nombre de cycles nécessaires pour afficher l'image avec un seul processeur. Combien d'instructions ont été exécutées pour afficher l'image? Quel est le nombre moyen de cycles par instruction (CPI) ?

Question D2 : Quel est le pourcentage d'écritures sur l'ensemble des instructions exécutées ? Quel est le pourcentage de lectures de données ? Quels sont les taux de miss sur le cache instruction et sur le cache de données ? Quel est le coüt d'un miss sur le cache instructions ? Quel est le coût d'un miss sur le cache de données ? Quel est le coût d'une écriture pour le processeur ? On rappelle que les couts se mesurent en nombre moyen de cycles de gel du processeur. Comment expliquez-vous que ces coûts ont des valeurs non entières?

Question D3 : Evaluez le nombre de transactions de chaque type pour cette application. Que remarquez-vous ?

E. Exécution sur architecture multi-processeurs

On s'intéresse maintenant à un scénario où l'application logicielle (construction et affichage de l'image) est exécutée - en parallèle - par plusieurs processeurs. Pour cela, on va partager le travail entre les processeurs, en utilisant le fait que les différentes lignes de l'image peuvent être traitées indépendamment les unes des autres: Dans une architecture contenant NPROCS processeurs, le processeur proc[i] se charge de la lignes (k) si (k%NPROCS == i). Si on a 2 processeurs par exemple, le processeur proc[0] traite les lignes paires, et le processeur proc[1] traite les lignes impaires.

Tous les processeurs exécutent donc le même programme main(), mais le travail effectué dépend du numéro du processeur. Le numéro de processeur est une valeur cablée au moment de la fabrication dans le registre protégé ($15 , 1). Cette valeur n'est donc pas modifiable par le logiciel, mais peut être lue, lorsque le processeur est en mode KERNEL, en utilisant l'instruction mfc0. En assembleur l'instruction suivante copie le registre protégé ($15 , 1) dans le registre non protégé ($27):

     mfc0 $27, $15, 1

Dans un programme utilisateur en C, cette information peut être obtenue en utilisant l'appel système procid().

Question E1 : Modifiez la boucle principale dans le fichier main.c pour partager le travail entre les différents processeurs de l'architecture.

Il faut également modifier le code de boot, car on a maintenant plusieurs tâches qui s'exécutent en parallèle, et chaque tâche doit disposer de sa propre pile d'exécution. Ici aussi, tous les processeurs exécutent le même code de boot, mais l'exécution dépend du numéro de processeur.

Question E2 : Pourquoi les piles d'exécution des N programmes s'exécutant sur les N processeurs doivent-elles être strictement disjointes ? Modifiez le fichier reset.s, pour initialiser le pointeur de pile à une valeur dépendant du numéro du processeur, de telle sorte que chaque tâche possède une pile de 64 Koctets, quelque soit le nombre de processeurs (compris entre 1 et 8).

Avec N processeurs, chaque processeur traite (256 / N) lignes au lieu de 256 lignes. Les N processeurs travaillant en parallèle, et indépendamment les uns des autres, le temps d'exécution total devrait être divisé par N.

On définit le speedup (accélération liée à la parallélisation sur N processeurs) comme suit :

speedup(N) = Temps de calcul sur 1 processeur / Temps de calcul sur N processeurs

Si speedup(N) = N quelle que soit la valeur de N, on dit qu'on a un speedup linéaire.

On va donc mesurer le speedup pour différentes architures matérielles comportant de 1 à 8 processeurs.

Question E3 : Donnez deux raisons pour lesquelles le code binaire doit être recompilé chaque fois qu'on change le nombre de processeurs.

Lancez successivement la simulation pour 1, 2, 4, 6, et 8 processeurs. Il faut d'abord modifier la valeur du paramètre NB_PROCS dans le fichier config.h et recompiler le logiciel), puis utiliser l'argument (-NPROCS) sur la ligne de commande du simulateur.

Question E4 Remplissez le tableau ci-dessous, et représentez graphiquement speedup(N) en fonction de N.

1 proc 2 procs 4 procs 6 procs 8 procs
cycles
speedup

Comment expliquez-vous que le speedup ne soit pas linéaire?

F. Evaluation des temps d'accès au bus

On cherche à vérifier l'hypothèse que c'est la saturation du bus, et plus précisément l'augmentation du temps d'accès au bus qui est responsable de la dégradation des performances, lorsque le nombre de processeurs augmente . On dispose pour cela de deux indicateurs :

  • La méthode printStatistics() associée au processeur permet de mesurer le coût moyen des différents types de transaction. Ce coût est obtenu en divisant le nombre total de cycles de gel du processeur par le nombre d'événements de chaque type.
  • La méthode printStatistics() associée au BCU permet de mesurer, pour chaque maître utilisant le bus, le temps moyen d'accès au bus, défini comme le nombre de cycles d'attente entre l'activation du signal REQ[i] (demande d'accès au bus par le maître (i)) et l'activation du signal GNT[i] (allocation du bus au maître (i)). On divise pour cela le nombre total de cycles d'attente du maître (i) par le nombre total de requêtes effectuées par le maître (i).

Il faut donc relancer les simulations pour les 5 architectures, en activant l'option -STATS sur la ligne de commande pour relever les coûts et les temps d'accès au bus.

question F1 Pourquoi faut-il absolument effectuer la mesure au moment précis où l'application se termine?

Question F2 Remplissez le tableau ci-dessous, en exécutant successivement le programme sur différentes architectures:

NPROCS 1 2 4 6 8
IMISS COST
DMISS COST
WRITE COST
ACCESS_TIME
CPI

Il faut tenir compte de la question précédente, car la durée d'exécution de l'application dépend du nombre de processeurs.

Question F3 : Interprétez ces résultats. La dégradation de la valeur du CPI quand on augmente le nombre de processeurs est-elle principalement dûe à l'augmentation du coût des MISS, ou à l'augmentation du coût des écritures?

G. Modélisation du comportement du bus

Les simulations effectuées dans les questions précédentes indiquent qu'il existe un seuil de saturation, au delà duquel l'augmentation du nombre de processeurs ne sert plus à rien, à cause de la bande passante limitée du bus. Nous allons maintenant calculer «théoriquement » (c'est à dire sans simulation) la valeur de ce seuil de saturation.

Question G1 : En exploitant votre compréhension du protocole PIBUS, calculez le nombre de cycles d'occupation du bus pour chacun des 4 types de transaction. Attention : il ne faut pas compter le temps d'attente pour accéder au bus, car la phase REQ->GNT n'utilise pas le bus et a lieu pendant la fin de la transaction précédente. Par contre, il faut compter un cycle supplémentaire correspondant au "cycle mort" entre deux transactions.

Question G2 : En exploitant les résultats de la partie D (pourcentage de lectures cachées, pourcentage d' écritures, taux de MISS sur les caches, valeur du CPI), calculez la fréquence de chacun des 4 types de transaction et complétez le tableau ci-dessous. Puisque notre unité de temps est le cycle, ces fréquences seront exprimées en nombre d'événements par cycle.

Temps_Occupation Fréquence
IMISS
DMISS
UNC
WRITE

On rappelle que pour n'importe quel type d'événement, la fréquence peut être calculée comme :

(nombre d'événement / cycle) = (nombre d'événement / instruction) * (nombre d'instruction / cycle)

Question G3 : En utilisant les résultats des deux questions précédentes, calculez le pourcentage de la bande passante du bus utilisé par un seul processeur. En déduire le nombre de processeurs au delà duquel le bus commence à saturer.

H. Compte-rendu

Les réponses aux questions ci-dessus doivent être rédigées sous éditeur de texte et ce compte-rendu doit être rendu au début de la séance de TP suivante. De même, le simulateur, fera l'objet d'un contrôle (par binôme) au début de la séance de TP de la semaine suivante.

Last modified 4 months ago Last modified on Mar 1, 2024, 3:08:24 PM

Attachments (2)

Download all attachments as: .zip