EXAMEN PARTIEL (solution)

Transcription

EXAMEN PARTIEL (solution)
Programmation parallèle et distribuée (GIF-4104/7104)
Département de Génie électrique et de Génie Informatique
Hiver 2012
EXAMEN PARTIEL
(solution)
Instructions : – 1 feuille aide-mémoire manuscrite est permise (recto-verso) ;
– répondre sur le questionnaire ; utilisez le verso des pages au besoin ;
– durée de l’examen : 110 minutes.
Pondération : Cet examen compte pour 35% de la note finale.
Question Points Score
1
15
2
20
3
10
4
10
5
5
6
10
7
20
8
10
Total:
100
Nom et prénom : __________________________________
Matricule : __________________________________
Question 1
(15 points sur 100)
Soit un programme multifilaire comportant entre autres choses une variable globale x initialisée à 0, ainsi qu’une fonction incr permettant d’incrémenter la valeur x :
int x = 0;
···
void incr() {
x = x+1;
}
(a) (10 points) Si n fils d’exécution («threads») appellent la fonction incr, y a-t-il un risque
de course critique ? Si oui, quelles sont les valeurs possibles pour x une fois les n appels
complétés ? Justifiez votre réponse.
Page 1 de 7
GIF-4104/7104
Examen partiel
28 février 2012
Solution: Oui, il y a un risque élevé de course critique, car tous les fils d’exécution vont
lire et écrire le même emplacement mémoire. Les valeurs possibles pour x vont de 1 à
n. En effet, si tous les fils viennent lire la valeur de x en même temps, ils liront tous 0 et
écriront tous 1 (ce cas est peu probable, mais néanmoins possible). En général, si m < n
fils viennent lire x en même temps, ils voudront tous écrire la même valeur en mémoire.
En supposant que les n − m autres fils s’exécutent séquentiellement, on obtiendra la
valeur x = n − m + 1, avec 0 ≤ m ≤ n.
(b) (5 points) Que faudrait-il faire pour que x prenne systématiquement la valeur x = n ?
Solution: Pour éviter tout risque de course critique, il faut verrouiller un mutex avant
d’incrémenter x, et le dévérouiller immédiatement après :
Question 2
(20 points sur 100)
Soit le programme suivant comportant deux fils d’exécution :
int x;
bool lDone = false;
···
fil #1
fil #2
···
int y;
x = · · ·;
···
lDone = true;
while(!lDone);
···
y = x;
(a) (5 points) Sachant que la variable lDone est booléenne, y a-t-il malgré tout un risque de
course critique ?
Solution: Non, le problème n’est pas vraiment celui d’une course critique. Il y a deux
problèmes. Le premier concerne le fait que le fil #2 va potentiellement consommer inutilement une grande quantité de cycles de calcul en attendant que le fil #1 mette la variable
lDone à true. Le second est que l’on ne sait pas ce que le compilateur va faire avec
la boucle du fil #2. En examinant cette boucle, il va sans doute constater que celle-ci
ne modifie pas la variable lDone et, par conséquent, va lire cette variable une fois et
mettre sa valeur dans un registre. Par la suite, sa valeur ne changera plus !
(b) (5 points) Y a-t-il un risque que le fil #2 ne sorte jamais de la boucle ?
Solution: Oui, il y a un risque important que la boucle soit infinie si celle-ci commence
à s’exécuter avant que la variable lDone prenne la valeur true (voir réponse précédente).
(c) (10 points) Que faut-il faire pour éviter tout problème ?
Solution: Il faut utiliser une condition et un mutex. Le fil #1 verrouille le mutex avant
d’écrire dans la variable x et de modifier lDone, puis déverrouille le mutex avant de
poursuivre son exécution. Le fil #2 verrouille le mutex avant d’exécuter la boucle et,
à l’intérieur de la boucle, attends après la condition. À la sortie de la boucle le fil #2
récupère la valeur de x puis déverrouille le mutex avant de poursuivre son exécution.
Page 2 de 7
GIF-4104/7104
Examen partiel
28 février 2012
Justifiez vos réponses.
Question 3 (10 points sur 100)
Soit un programme possédant une fonction qui ne peut pas être parallélisée. Si cette fonction
nécessite x% du temps total d’exécution, quelle est la borne supérieure pour le «speedup» du
programme sur un ordinateur à p processeurs ? Justifiez votre réponse.
Solution: La loi d’Amdahl spécifie le «speedup» maximal lorsque seule une portion α du
code séquentiel peut être parallélisée :
speedup ≤
1
1−α
Or, nous avons α = 1 − x et
speedup ≤
1
x
.
Question 4
(10 points sur 100)
Soit un réseau de communication possédant les propriétés suivantes :
– bande passante : x octets/seconde ;
– latence : y secondes.
Calculez le débit maximal (en messages/seconde) de cette réseautique pour des messages de
longueur n ?
Solution: La réponse à cette question peut-être fort complexe, car elle dépend en fait de
plusieurs facteurs non inclus dans son énoncé. Ceci étant dit, la latence spécifie le délai
nécessaire pour transmettre un message de longueur minimale (1 octet). Pour transmettre un
message de n octets, il faut donc attendre le délai de la latence plus celui de la transmission
des n octets du message. Au total, cela demande un temps
t = y + n/x
et le nombre de messages par seconde vu, sous cet angle pessimiste, sera de
1
y + n/x
Sous un angle plus optimiste, on pourrait aussi répondre qu’en enfilant plusieurs messages
consécutifs dans une forme de pipeline, seule la latence du premier message compterait et
nous pourrions alors atteindre un débit maximum de
1
x
=
n/x
n
La réponse en pratique se situera entre les deux valeurs précédentes.
Question 5
(5 points sur 100)
Expliquez comment faire pour mesurer la latence dans un réseau de communication ?
Page 3 de 7
GIF-4104/7104
Examen partiel
28 février 2012
Solution: On peut faire un ping-pong (échange d’un message entre deux processus) avec
un message de 1 octet, mesurer le temps du ping-pong et diviser par deux. Pour plus de
précision, on mesure habituellement plusieurs ping-pong et on prend la moyenne des temps.
Question 6
(10 points sur 100)
Soit le code séquentiel suivant permettant d’effectuer le produit d’une matrice triangulaire par
un vecteur (la classe Matrix est celle du tp1) :
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "Matrix.hpp"
using namespace std;
valarray<double>
multTriangMatrix(const Matrix& inM, const valarray<double>& inV) {
valarray<double> lRes(inM.rows());
for (size_t i=0; i < lRes.size(); ++i) {
lRes[i] = 0;
for (size_t j=i; j < inV.size(); ++j) {
lRes[i] += inM(i,j)*inV[j];
}
}
return lRes;
}
En utilisant OpenMP, parallélisez ce code de manière à minimiser son temps d’exécution sur
un ordinateur multicœur. Notez bien que la matrice est supposée triangulaire, c’est-à-dire que
tous les éléments situés en dessous de sa diagonale son zéro (voir illustration).
Solution: Pour paralléliser ce code avec OpenMP, il suffit d’ajouter deux directives au compilateur :
#pragma omp parallel shared(inM, inV, lRes)
{
#pragma omp for schedule(dynamic)
lignes 6 à 11
}
1. Une première directive pour définir une région parallèle qui couvre les luges 6 à 11.
Cette directive précise que les variables inM, inV et lRes peuvent être partagée,
puisque chaque fil d’exécution utilisera des valeurs distinctes pour la variable i.
Page 4 de 7
GIF-4104/7104
Examen partiel
28 février 2012
2. Une seconde directive pour paralléliser la boucle de la ligne 6. Il importe de préciser un ordonnancement dynamique, puisque la longueur des lignes diminue au fil des
itérations et le temps d’exécution des itérations sera donc variable.
Question 7
(20 points sur 100)
En utilisant MPI, écrivez une fonction pour déterminer le temps de transmission d’un message
entre deux processus. Utilisez le prototype suivant :
double computePingPong(unsigned iBufSize, unsigned iRepeat);
Le processus 0 transmet un message de iBufSize octets au processus 1 (étape «ping») et
celui-ci retourne le message reçu au processus 0 (étape «pong»). En répétant iRepeat fois
cet échange et en chronométrant le temps, on peut calculer avec une assez bonne précision le
temps moyen de transmission d’un seul message. Écrivez le code C/C++ de cette fonction.
Solution: On suppose que MPI est déjà initialisé :
1
2
3
4
5
6
7
8
double computePingPong(unsigned iBufSize, unsigned iRepeat=1)
{
// check number of processes and determine rank
if( MPI::COMM_WORLD.Get_size() < 2 ) {
cerr << "Program must run with at least 2 processes!" <<
endl;
exit(-1);
}
int lRank = MPI::COMM_WORLD.Get_rank();
9
10
11
12
13
14
// allocate send/recv buffers
char* lBuffer = (char*) new char[inBufSize];
for(unsigned int i = 0; i < inBufSize; ++i) {
lBuffer[i] = (char) i%256;
}
15
16
17
18
19
20
21
22
23
24
25
26
double lSum = 0;
for(unsigned int i = 0; i < iRepeat; ++i) {
double lStartTime = MPI::Wtime();
if( lRank == 0 ) {
// send ping-pong
MPI::COMM_WORLD.Send(lBuffer, inBufSize,
1);
MPI::COMM_WORLD.Recv(lBuffer, inBufSize,
1, 0);
else if( lRank == 1 ) {
// return pong-ping
MPI::COMM_WORLD.Recv(lBuffer, inBufSize,
1, 0);
MPI::COMM_WORLD.Send(lBuffer, inBufSize,
1);
Page 5 de 7
MPI::CHAR, 1,
MPI::CHAR, 1,
MPI::CHAR, 0,
MPI::CHAR, 0,
GIF-4104/7104
Examen partiel
28 février 2012
}
lSum += MPI::Wtime()-lStartTime;
27
28
}
29
30
// free memory and return average divided by 2
delete [] lBuffer;
return lSum/iRepeat/2;
31
32
33
34
}
Question 8
(10 points sur 100)
Soit le code suivant qui définit une structure de pile dans un tableau dynamique :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <vector>
#include <stdexcept>
template <class T>
class Stack {
public:
Stack() : mTable(1), mTop(-1) {}
bool empty() const { return mTop == -1; } // tester si vide
void pop() { // retirer l’element du dessus
if (empty()) throw std::runtime_error("empty stack!");
mTop--;
}
void push(const T& iVal) { // ajouter un element au dessus
if (mTop == mTable.size()-1) {
mTable.resize(2*mTable.size()+1);
}
mTable[++mTop] = iVal;
}
const T& top() const { // retourner l’element du dessus
if (empty()) throw std::runtime_error("empty stack!");
return mTable[mTop];
}
private:
std::vector<T> mTable; // table des elements
int mTop;
// indice de l’element du dessus
};
Décrivez en détails les changements à effectuer pour pouvoir utiliser cette structure de pile
dans un contexte multifilaire, sans risque de course critique ?
Solution: Il faut d’abord ajouter un mutex comme membre de la classe et initialiser celuici dans le constructeur. Ensuite, pour les fonctions qui modifient l’état de la classe, c-à-d
les fonctions non const, il faut verrouiller le mutex au début de chaque fonction (pop et
push) et le déverrouiller avant de faire le return. Pour les fonctions const (top en
particulier), c’est plus délicat. On peut toujours verrouiller le mutex avant de déterminer le
Page 6 de 7
GIF-4104/7104
Examen partiel
28 février 2012
valeur de retour, mais cela ne garantit pas que le résultat sera encore valide une fois retourné
à la fonction appelante. Par exemple, si je consulte avec top la valeur sur le dessus de la
pile, et que je décide de faire un pop pour retirer celle-ci, il se peut très bien que je retire en
fait une autre valeur si un autre fil a exécuté un pop ou un push entre temps. Pour palier
à ce problème, on pourrait retirer la méthode top et modifier la méthode pop pour quelle
retourne la valeur retirée. Une alternative serait d’ajouter des méthodes lock et unlock
pour permettre à l’utilisateur de consulter le dessus de la pile sans risque qu’un autre fil ajoute
ou retire quelque chose entre temps. Par contre, pour cette dernière alternative, il faudrait
prendre soin d’utiliser un mutex récursif, c-à-d un mutex qu’un même fil peut verrouiller
plusieurs fois.
Page 7 de 7