Rapport de projet de développement logiciel

Transcription

Rapport de projet de développement logiciel
Rapport de projet de développement logiciel
Bruno Roggeri & Rémi Lallement
20 janvier 2005
Résolution du problème des N reines en multithreading
Table des matières
1 Dossier de spécification
1.1 Objectif . . . . . . . . . . .
1.2 Cas d’utilisation . . . . . .
1.3 Scenario . . . . . . . . . . .
1.4 Cahier des charges . . . . .
1.4.1 Objectifs principaux
1.4.2 Objectifs secondaires
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2 Plan des tests
3 Dossier de conception
3.1 Architecture du programme . . . . . . . . . . . . . . . . . . . . . . .
3.1.1 Interactions entre les interfaces utilisateurs et les méthodes
résolution . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.2 Gestion des paramètres . . . . . . . . . . . . . . . . . . . . .
3.1.3 Diagramme des classes . . . . . . . . . . . . . . . . . . . . . .
3.2 Algorithmes et méthodes de parallélisation . . . . . . . . . . . . . . .
3.2.1 Méthode de résolution séquentielle par backtracking primaire
3.2.1.1 Principe de l’algorithme . . . . . . . . . . . . . . . .
3.2.1.2 Représentation des données . . . . . . . . . . . . . .
3.2.2 Méthode de résolution séquentielle par backtracking utilisant
colonnes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.2.1 Principe de l’algorithme . . . . . . . . . . . . . . . .
3.2.2.2 Réprésentation des données . . . . . . . . . . . . . .
3.2.3 Méthode de résolution séquentielle par backtracking utilisant
colonnes et les diagonales . . . . . . . . . . . . . . . . . . . .
3.2.3.1 Principe de l’algorithme . . . . . . . . . . . . . . . .
3.2.3.2 Réprésentation des données . . . . . . . . . . . . . .
3.2.4 Utilisation des symétries . . . . . . . . . . . . . . . . . . . . .
3.2.4.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.5 Méthode de résolution parallélisée statiquement . . . . . . . .
3.2.5.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.5.2 Contraintes . . . . . . . . . . . . . . . . . . . . . . .
3.2.5.3 Evolution . . . . . . . . . . . . . . . . . . . . . . . .
3.2.6 Méthode de résolution parallélisée dynamiquement . . . . . .
3.2.6.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
7
8
8
8
8
11
15
. . . 15
de
. . . 15
. . . 16
. . . 16
. . . 17
. . . 17
. . . 17
. . . 18
les
. . . 18
. . . 18
. . . 18
les
. . . 18
. . . 18
. . . 18
. . . 19
. . . 19
. . . 19
. . . 19
. . . 19
. . . 19
. . . 20
. . . 20
3
3.3
3.2.6.2 Diagrammes des classes et de déploiement des objets
3.2.6.3 Exemple de diagramme de séquence . . . . . . . . . .
Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3.1 L’interface graphique . . . . . . . . . . . . . . . . . . . . . . . .
3.3.2 L’interface console . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
4 Tests
21
22
22
22
23
25
5 Documentation
27
5.1 Manuel de l’interface console . . . . . . . . . . . . . . . . . . . . . . . . . 27
5.2 Manuel de l’interface graphique . . . . . . . . . . . . . . . . . . . . . . . . 28
6 Expérimentations
6.1 Comportement des différentes implémentations de la machine virtuelle . .
6.1.1 Test du compteur multithreadé . . . . . . . . . . . . . . . . . . . .
6.1.1.1 Protocole de mesure . . . . . . . . . . . . . . . . . . . . .
6.1.1.2 Comparaison des courbes de temps en fonction du nombre
de threads . . . . . . . . . . . . . . . . . . . . . . . . . .
6.1.1.3 Speed-ups de niveau 1 . . . . . . . . . . . . . . . . . . . .
6.1.1.4 Speed-ups de niveau 2 . . . . . . . . . . . . . . . . . . . .
6.1.2 Utilisation réelle avec le problème des 14 reines . . . . . . . . . . .
6.1.2.1 Speed-ups de niveau 1 . . . . . . . . . . . . . . . . . . . .
6.1.2.2 Speed-ups de niveau 2 . . . . . . . . . . . . . . . . . . . .
6.2 Paramètres optimaux pour la parallélisation dynamique . . . . . . . . . .
4
31
31
31
32
35
36
39
40
41
43
45
Introduction
Le problème des N reines consiste à placer, sur un damier carré de N fois N cases, N
reines (au sens du jeu d’echecs) sans qu’aucunes d’elles ne soient en prise.
Exemple de solution pour N=4 :
Fig. 0.1: Solution possible du problème des 4 reines
L’objet de ce projet est de trouver un algorithme pouvant utiliser les capacités d’une
machine multi-processeurs permettant de résoudre le problème le plus rapidement possible.
5
6
Dossier de spécification
1
1.1 Objectif
Le but de ce projet est bien sûr de résoudre le problème des N reines, et cela le plus
rapidement possible, en utilisant éventuellement les possibilités offertes par les machines
multi-processeurs. A cette fin, nous souhaitons développer une interface aussi pratique
que possible qui nous permettra de tester facilement différentes méthodes de résolution
et de gestion des threads, de comparer leurs performances et de sauvegarder les résultats
obtenus.
1.2 Cas d’utilisation
Le logiciel ne se destine qu’à un seul type d’acteur : les utilisateurs souhaitant résoudre
le problème des N reines. Il doivent pouvoir choisir la taille du problème, la méthode de
résolution, ses paramètres, et le traitement éventuel des résultats obtenus.
Fig. 1.1: Cas d’utilisation
7
1.3 Scenario
Fig. 1.2: Scenario
1.4 Cahier des charges
1.4.1 Objectifs principaux
Nous souhaitons élaborer plusieurs méthodes de résolution et disposer d’un outil
permettant de comparer facilement leurs performances. En particulier nous souhaitons
développer une interface utilisateur graphique permettant de choisir simplement la taille
du problème et la méthode de résolution ainsi que ses éventuels paramètres, et permettant de mesurer les temps d’exécution.
Nous nous fixons comme objectif de réaliser au moins un de chacun des principaux
types d’algorithme suivants :
– Un algorithme séquentiel monothread
– Un algorithme multithread avec gestion statique des threads
– Un algorithme multithread avec gestion dynamique des threads
1.4.2 Objectifs secondaires
Selon le temps qui aura été nécéssaire à la réalisation des objectifs principaux, nous
aimerions pouvoir essayer plusieurs méthodes d’un même type pour tenter d’optimiser
8
les performances (exemple : pour un algorithme avec gestion statique des threads, jouer
sur les découpages possibles de l’arbre de recherche).
Eventuellement, nous pourrions aussi regarder s’il est possible d’introduire une part
de récursivité dans la résolution du problème des N reines, c’est-à-dire s’il est possible
de réutiliser les solutions du problèmes pour un N inférieur.
On pourrait aussi s’intéresser à des manières amusantes d’afficher les solutions trouvées
et d’illustrer les algorithmes utilisés. Enfin, il serait intéressant de pouvoir créer des
journaux gardant une trace des résultats obtenus, avec la méthode de résolution, le
temps d’exécution et la machine utilisée.
9
10
Plan des tests
2
Nous avons ici séparé les tests concernant l’interface graphique, l’interface console, et
les différentes méthodes de résolution.
11
#ID
IG-1
IG-2
Test
Démarrage : affichage de la fenêtre
Sélection de la taille du damier
IG-3
Sélection de la méthode
IG-4
IG-6.2
IG-6.3
Affichage dynamique des
paramètres
Selection des paramètres
supplémentaires
Paramètre de la méthode
parallélisée statique
Paramètre de la méthode
parallélisée statique + sym
Paramètres de la méthode
parallélisée dynamiquement
Lancement du calcul
Désactivation du bouton
« Lancer »
Affichage : « Execution en cours »
Indicateur d’activité
IG-7
IG-8
IG-9
Fin du calcul : affichage du résultat
Réactivation du bouton « Lancer »
Affichage du temps d’éxecution
IG-10
Fermeture du programme
IG-5
IG-5.1
IG-5.2
IG-5.3
IG-6
IG-6.1
Technique de test
Lancement du programme
Action sur le menu déroulant
correspondant
Action sur le menu déroulant
correspondant
Sélection successive de différentes
méthodes
Tests répétés pour chaque méthode
l’exigeant
Action sur le menu déroulant
correspondant
Action sur le menu déroulant
correspondant
Action sur le menu déroulant
correspondant
Action sur le bouton « Lancer »
Observation directe
Observation directe
Observation : l’indicateur doit
clignoter
Observation directe
Observation directe
Observations répétées, cohérence,
et chronométrage
Fermer la fenêtre, vérifier au
niveau de l’OS
Tab. 2.1: Tests de l’interface graphique
12
#ID
IC-1
IC-2
Test
Démarrage : affichage du message
d’accueil
Affichage des messages d’aide
IC-3
Sélection de la taille du damier
IC-4
Sélection de la méthode
IC-5
IC-6
IC-7
Sélection
des
paramètres
supplémentaires
Indicateur d’activité
Affichage des solutions
IC-8
IC-9
Affichage du nombre de solutions
Affichage du temps d’éxecution
IC-10
Fermeture du programme à la fin du
calcul
Technique de test
Lancement du programme
Tests avec paramètres erronés, manquants ou superflus
Lancement avec des paramètres corrects
Lancement avec des paramètres corrects
Lancement avec des paramètres corrects
Observation
Tests avec les premières méthodes
séquentielles
Observation directe
Observations répétées, cohérence, et
chronométrage
Vérification au niveau de l’OS
Tab. 2.2: Tests de l’interface console
13
#ID
M-1
M-2
M-3
M-4
M-5
M-6
M-7
Test
Résultat par la méthode séquentielle
de backtracking simple
Résultat par la méthode introduisant le placement par colonne
Résultat par la méthode introduisant le stockage des états des lignes
et des diagonales
Résultat par la méthode introduisant l’utilisation de la symétrie
Résultat par la méthode parallélisée
de manière statique n’utilisant pas
les symétries
Résultat par la méthode parallélisée
de manière statique utilisant les
symétries
Résultat par la méthode parallélisée
de manière dynamique
Technique de test
Comparaison avec la littérature et
les autres méthodes
Comparaison avec la littérature et
les autres méthodes
Comparaison avec la littérature et
les autres méthodes
Comparaison avec la littérature et
les autres méthodes
Comparaison avec la littérature et
les autres méthodes
Comparaison avec la littérature et
les autres méthodes
Comparaison avec la littérature et
les autres méthodes
Tab. 2.3: Tests des méthodes
14
Dossier de conception
3
3.1 Architecture du programme
Dans le but de pouvoir facilement ajouter de nouvelles méthodes de résolution et
éventuellement de nouvelles interfaces, le programme a été architecturé de manière à
être modulaire.
3.1.1 Interactions entre les interfaces utilisateurs et les méthodes de
résolution
Toutes les méthodes de résolution sont des classes implémentant l’interface MethodeDeResolution. Cette interface demande la définition de quelques opérations permettant
l’identification de la méthode et la description des paramètres qu’elle nécessite, mais
surtout impose la création d’une opération lancer. Celle-ci effectuera la résolution du
problème des N reines en utilisant la méthode en question.
Réciproquement, toutes les interfaces pouvant être utilisées pour choisir une méthode
de résolution ainsi que ses paramètres, la lancer, et gérer le retour des résultats à
l’utilisateur, doivent implémenter l’interface UInterface. Cette interface impose trois
opérations. La plus importante est l’opération afficherNbreSolutions, qui sera utilisée
par une méthode de résolution pour retourner le nombre de solutions trouvées à la
fin du calcul. Les deux autres sont « optionnelles », au sens où il n’est pas primordial qu’elles effectuent une action, ni qu’une méthode de résolution ne les déclenche.
L’opération afficherSolution permet le retour éventuel d’une représentation d’une solution vers l’interface sous la forme d’une matrice N ×N de booléens, tandis que l’opération
signalerSolution permet de signaler la découverte d’une nouvelle solution et peut servir
à implémenter un indicateur d’activité sans pour autant spécifier la solution trouvée.
Souvent, nous n’utiliserons pas la méthode afficherSolution car elle peut s’avérer assez
coûteuse. Elle n’est d’ailleurs pas implémentée dans l’interface graphique que nous avons
créée, mais seulement dans l’interface console, et seules les premières méthodes y font
effectivement appel.
De même, signalerSolution n’est implémentée que dans l’interface graphique, car nous
ne nous sommes pas donnés la peine de doter l’interface console d’un indicateur d’activité. Elle est par contre utilisée par toutes les méthodes de résolutions que nous avons
développées.
15
Pour que chaque interface soit automatiquement associée à toutes les méthodes existantes, nous avons de plus créé une classe intermédiaire Methodes qui rassemble toutes
les méthodes de résolution dans un tableau. Ainsi, en ne modifiant que cette dernière
classe, toutes les interfaces ont directement accès à l’ensemble des méthodes.
3.1.2 Gestion des paramètres
Dans un premier temps, les méthodes que nous avions implémentées ne demandaient
qu’un seul paramètre évidemment indispensable : N , le nombre de cases sur un côté du
damier, ou autrement dit le nombre de reines à placer dessus. Ce paramètre était donc
naturellement placé en argument de la méthode lancer.
Mais quand nous avons commencé à développer des méthodes parallélisées, nous
avons eu besoin d’introduire un autre paramètre : le nombre de threads. En anticipant l’implémentation de nouvelles méthodes qui pourraient nécessiter un nombre arbitraire de paramètres autres que la taille du problème, nous avons voulu doter notre
programme d’un mécanisme permettant aux interfaces de s’adapter automatiquement à
chaque méthode de résolution.
Pour ce faire, nous avons créé une classe Parametres permettant de décrire d’une
manière standard les paramètres d’une méthode, et associé à chaque méthode de résolution
une instance de cette classe. Les interfaces pourront donc, pour chaque méthode de
résolution, récupérer la description des paramètres qui lui sont nécessaires et s’adapter
pour permettre à l’utilisateur de spécifier ces paramètres. La description des paramètres
comprend le nombre de paramètres attendus, leurs noms et l’ensemble des valeurs qu’ils
peuvent prendre. A charge de l’interface de s’assurer que les paramètres spécifiés par
l’utilisateur sont valides.
Cette même classe permet à l’interface de communiquer les paramètres choisis par
l’utilisateur à la méthode de résolution avant l’appel de l’opération lancer, ceci à l’aide de
l’opération setParametre qui permet de donner une certaine valeur à un des paramètres
de la méthode. La méthode de résolution pourra alors à son tour récupérer les paramètres
choisis grâce à l’opération getParametre.
Il est vrai que, en pratique, nous n’avons pas implémenté de méthodes demandant plus
d’un autre paramètre que N . Notre démarche peut donc sembler un peu trop compliquée
par rapport à nos besoins. Toutefois, généraliser le problème à ce niveau nous a permis
de conserver un code plus simple au sens où nous n’avons pas dû nous embarrasser de
la gestion des différents cas indépendemment les uns des autres, ce qui aurait brisé la
modularité du programme.
3.1.3 Diagramme des classes
Le diagramme des classes suivant décrit cette l’architecture. On peut y voir plus
précisément comment la classe Parametres a été implémentée, ainsi que toutes les interactions décrites dans les paragraphes précédents.
16
Fig. 3.1: Diagramme des classes
3.2 Algorithmes et méthodes de parallélisation
3.2.1 Méthode de résolution séquentielle par backtracking primaire
3.2.1.1 Principe de l’algorithme
S’il y a de la place sur le damier pour une nouvelle reine, on place une nouvelle reine
dans une des places libres, et on recommence. Si par contre il n’y a plus de place, on
vérifie d’une part que toutes les reines n’ont pas été placées (auquel cas on vient de
trouver une solution), et d’autre part qu’il reste bien des reines sur le damier. S’il n’en
reste aucune, c’est fini, sinon, on enlève la dernière reine placée, on marque l’endroit où
elle était comme étant inutilisable, et on recommence.
17
3.2.1.2 Représentation des données
Dans cet algorithme, on représente le damier par une matrice N ×N de booléens. Si un
élément de la matrice est VRAI, la case correspondante est inutilisable, soit parce qu’une
reine est dessus, soit parce qu’une reine la menace, soit parce qu’on a délibérement coché
la case pour ne pas y revenir.
3.2.2 Méthode de résolution séquentielle par backtracking utilisant les
colonnes
3.2.2.1 Principe de l’algorithme
On utilise le fait que toutes les solutions auront une seule reine par colonne. On cherche
donc à placer chaque reine sur sa colonne.
Si la reine de la prochaine colonne peut être placée sans être en prise avec une des
reines déjà placées dans une des colonnes précédentes, alors on l’y met, et on passe à la
reine de la colonne suivante. Si par contre ce n’est pas possible, on revient à la reine de
la colonne précédente, en on recommence.
On a une solution dès qu’on a pu placer chaque reine sur sa colonne. Dans ce cas on
continue comme si la prochaine reine ne pouvait pas être placée (il n’y a d’ailleurs pas
de prochaine reine), donc en cherchant une nouvelle position pour la reine de la dernière
colonne. L’algorithme se termine quand on n’arrive plus à placer la première reine.
3.2.2.2 Réprésentation des données
L’état du damier est représenté par un tableau de taille N contenant les positions de
chacune des reines dans leur colonne.
3.2.3 Méthode de résolution séquentielle par backtracking utilisant les
colonnes et les diagonales
3.2.3.1 Principe de l’algorithme
Même principe que l’algorithme précédent, sauf que l’on prend note des lignes et des
diagonales occupées, ce qui permet de tester très rapidement si une position est valable,
au lieu de devoir vérifier les interactions avec chacune des reines déjà placées.
3.2.3.2 Réprésentation des données
L’état du damier est représenté par un tableau d’entiers de taille N contenant les
positions de chacune des reines dans leur colonne. De plus, 3 autres tableaux de booléens
de taille N servent à noter quelles sont les lignes / diagonales / antidiagonales libres.
18
3.2.4 Utilisation des symétries
3.2.4.1 Principe
On reprend l’algorithme, mais on prend garde maintenant à ne calculer que les solutions dont la première reine se situe sur la première moitié de sa colonne. Les autres
solutions s’obtiennent par symétrie verticale. Pour les tailles de damier impair, on rajoute
le nombre de solutions dont la première reine se situe au milieu de sa colonne. Ce nombre
est calculé en utilisant à nouveau la symétrie verticale, qui a été conservée puisque la
première reine est au milieu. On ne considère donc que les solutions dont la deuxième
reine est sur la première moitié de sa colonne. On peut même systématiquement éviter
la dernière position de la première moitié puisque la première reine la bloque.
Fig. 3.2: Utilisation de la symétrie
3.2.5 Méthode de résolution parallélisée statiquement
3.2.5.1 Principe
L’algorithme est le même que celui utilisé pour la méthode du paragraphe 3.2.3. Mais
on introduit maintenant un nouveau paramètre : le nombre de threads sur lesquels
nous allons répartir la charge de travail. La répartition est faite de manière statique :
chaque thread se voit assigner au démarrage du programme une partie du problème total
caractérisé par l’ensemble des positions de la première reine qu’il doit explorer.
3.2.5.2 Contraintes
La répartition en différents threads n’est pas gratuite : pour chaque nouveau thread il
faut recréer un environnement (tableau des positions et tableaux gardant trace de l’état
des lignes et des diagonales) qui lui soit propre afin que les threads n’interfèrent pas
entre eux
3.2.5.3 Evolution
On peut bien sûr utiliser les mêmes symétries proposées au 3.2.4.
19
3.2.6 Méthode de résolution parallélisée dynamiquement
3.2.6.1 Principe
La méthode de parallélisation précédente avait l’inconvénient de donner aux différents
threads des tâches qui pouvaient être de longueurs assez différentes les unes des autres.
Un thread qui finissait sa tâche bien avant les autres ne faisait alors plus aucun travail.
Une solution pouvait consister à lancer plus de threads qu’il n’ y a de processeurs, de sorte
à avoir des tâches plus petites et donc potentiellement de longueurs moins disparates.
Mais les performances dépendent alors grandement du comportement de l’ordonnanceur,
qui s’est avéré être assez aléatoire. De plus, cette astuce augmente le temps passé à créer
des threads.
Pour pallier à ce problème, nous avons utilisé un système de producteur / consommateurs. Le thread principal de la méthode va se charger de diviser le problème initial en
sous-problèmes plus petits et les mettre dans une file d’attente, tenant ainsi le rôle de
producteur. Cette tâche est ici accomplie en plaçant un certain nombre d des premières
reines de manière séquentielle. Un nouveau problème à traiter est ainsi mis en file à
chaque fois que le producteur arrive à placer d reines. d est caractéristique du degré de
division du problème initial.
Les threads consommateurs iront chercher dans cette file d’attente un sous-problème
et le traiteront. Dès qu’un thread consommateur a terminé sa tâche, il peut sans attendre
piocher un autre problème dans la file d’attente. Ainsi, le temps durant lequel un thread
peut se retrouver sans travail alors qu’il en reste encore à faire est très faible.
Cette méthode accepte donc deux paramètres dont il pourra être intéressant d’étudier
l’influence sur les performances : le nombre de threads consommateurs p et le degré de
division d.
Fig. 3.3: Schéma de la parallélisation dynamique
Dans le schéma ci-dessus, les zones du damier en traits solides correspondent à l’espace
20
dans lequel le thread en question doit placer des reines.
3.2.6.2 Diagrammes des classes et de déploiement des objets
Fig. 3.4: Diagramme des classes
Fig. 3.5: Schéma de déploiement des objets
21
3.2.6.3 Exemple de diagramme de séquence
Voici un diagramme de séquence pour le scénario où l’utilisateur lance la méthode de
résolution parallélisée dynamique par l’intermédiaire de l’interface console.
3.3 Interfaces
3.3.1 L’interface graphique
Losque nous avons commencé à développer l’interface graphique, nous ne disposions
que de méthodes séquentielles de résolution. Ainsi, pour ce qui est du choix des paramètres, nous n’avions qu’à nous occuper de la taille du damier, et du choix de la
méthode. Nous avons donc divisé l’unique fenêtre de notre interface en deux panneaux :
l’un contenant les boı̂tes dédiées à la sélection des différents paramètres ainsi qu’un
22
bouton servant à lancer le calcul, et l’autre où s’affichaient les résultats, c’est-à-dire le
nombre de solutions trouvées et la durée d’éxécution.
Lors du calcul des solutions pour un problème de taille relativement élevée (typiquement, à partir de N = 15), il est apparu que la présence d’un « indicateur d’activité »
serait pertinente afin de savoir, quand l’éxécution s’éternise un peu, si le programme
continue de tourner ou si la machine a planté. Il se présente sous la forme d’un caractère, visible sur le panneau contenant les résultats du calcul, qui prend tour à tour
la forme * ou # au fur et à mesure que des solutions au problème sont trouvées. Nous
avons pour cela ajouté l’opération signalerSolution dans la classe UInterface.
Quand il a fallu rendre les méthodes de résolution parallélisées disponibles depuis
l’interface graphique, nous avons dû ajouter un troisième panneau à la fenêtre, afin
de pouvoir rentrer de nouveaux paramètres, comme le nombre de threads utilisés, par
exemple. Comme nous l’avons expliqué au paragraphe 3.1.2, nous avons créé une classe
Parametres afin que les interfaces aient accès facilement aux paramètres exigés par les
différentes méthodes de résolution. Dans le cas de l’interface graphique, ceci a pour effet
de rendre visible dans le troisième panneau des boı̂tes permettant la sélection de ces
paramètres. Cet affichage se met à jour à chaque fois qu’une méthode est sélectionnée.
3.3.2 L’interface console
L’interface console a deux utilités. D’une part, elle a pu être developpée rapidement et
a servi pour faire les premiers tests. Une fois que l’architecture du programme explicitée
dans la section 3.1 a été mise en place de façon satisfaisante, nous avons pu coder les
méthodes de résolution et les interfaces en parallèle. Mais jusque là, c’était la seule interface que nous pouvions utiliser. D’autre part, l’intérêt de notre projet étant d’utiliser des
machines multi-processeurs, qui en l’occurence ne proposaient pas forcément d’interface
graphique, il était capital de disposer d’une interface qui fonctionnerait sur un maximum
de machines.
Le principe de l’interface est classique : les paramètres (taille du problème, numéro
de méthode, puis paramètres supplémentaires éventuels) sont passés directement sur la
ligne de commande. Si les paramètres ne sont pas valides, un message expliquant le
fonctionnement de l’interface est imprimé à l’écran.
23
24
4
Tests
#ID
IG-1
IG-2
IG-3
IG-4
IG-5
IG-5.1
IG-5.2
IG-5.3
IG-6
IG-6.1
IG-6.2
IG-6.3
IG-7
IG-8
IG-9
IG-10
IC-1
IC-2
Réussite
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Oui
Non
IC-3
IC-4
IC-5
IC-6
Oui
Oui
Oui
Non
IC-7
IC-8
IC-9
IC-10
M-1
Oui
Oui
Oui
Oui
Oui
Commentaires
La signification et le nombre de paramètres supplémentaires
des méthodes en nécessitant n’est pas affichée. Tous les
autres paramètres sont cependant expliqués.
L’indicateur n’est pas implémenté. Nous ne souhaitons pas
vraiment le faire afin de disposer d’une interface qui ne ralentit pas le calcul.
25
M-2
M-3
M-4
M-5
M-6
M-7
26
Oui
Oui
Oui
Oui
Oui
Oui
Documentation
5
5.1 Manuel de l’interface console
Cette interface s’utilise entièrement en mode texte : tous les paramètres sont directement passés sur la ligne de commande. La syntaxe est relativement simple :
$ java InterfaceConsole taille damier n méthode [param méthode1, ... ]
Les paramètres supplémentaires des méthodes qui en utilisent sont obligatoires. L’entrée
de paramètres non valides ou l’omission de paramètres entraı̂ne l’affichage d’un message
rappelant la syntaxe de la commande.
Les numéros des méthodes peuvent être obtenus en tapant simplement :
$ java InterfaceConsole
Ce qui donnera par exemple :
Utilisation : java InterfaceConsole taille damier methode choisie
[autres paramètres obligatoires si la méthode les requiert].
taille damier doit ^
etre un entier strictement positif
methode choisie doit correspondre à une de ces méthodes :
0 : Résolution séquentielle par backtracking primaire
1 : Résolution séquentielle par backtracking utilisant les colonnes
2 : Résolution séquentielle par backtracking utilisant les colonnes
et les diagonales
3 : Résolution séquentielle par backtracking utilisant des symétries
du problème
4 : Résolution Parallélisée de manière statique
5 : Résolution Parallélisée de manière statique utilisant une partie
des symétries
6 : Résolution Parallélisée de manière dynamique
27
5.2 Manuel de l’interface graphique
On lance cette interface en tapant :
$ java InterfaceGraphique
S’affiche alors la fenêtre suivante :
On choisit la taille du problème à traiter et la méthode de résolution grâce aux
boı̂tes correspondantes. Lorsque la méthode sélectionnée requiert un ou des pramètres
supplémentaires, de nouvelles boı̂tes de sélection s’affichent, comme ci-dessous.
Cliquez alors sur le bouton « Go ! » pour lancer le calcul. Ce bouton est désactivé pendant la recherche du nombre de solutions. Un message « Exécution en cours » s’affiche.
Si le calcul prend un certain temps, un indicateur d’activité se mettra à clignoter (un
caractère qui prend successivement les formes * et #).
28
A la fin du calcul, le nombre de solutions et le temps d’exécution s’affichent. De plus,
le bouton « Go ! » redevient actif afin qu’une nouvelle éxécution puisse être lancée.
Le programme est quitté par simple fermeture de la fenêtre.
29
30
Expérimentations
6
La multiplication des méthodes de résolutions développées ci-dessus, de leur paramètres
éventuels, des machines physiques sur lequel le code a pu être testé, et enfin des implémentations
de la machine virtuelle Java, nous a tenu un moment à l’écart du projet initial : résoudre
le problème des N reines le plus rapidement possible.
Nous n’avons donc pas pu nous lancer dans un comparatif exhaustif de toutes les
configurations possibles, mais nous nous sommes plutôt concentrés sur 2 points qui nous
ont semblé intéressants.
Premièrement, suite à un petit problème pendant le développement, nous nous sommes
aperçus que plusieurs machines virtuelles (ou VM) pouvaient avoir des comportements
sensiblement différents, notamment en ce qui concerne la gestion des threads. En effet,
certaines VM implémentent leur propre ordonnanceur de threads, et les font apparaı̂tre
au niveau de l’OS comme un unique thread, ce qui empêche toute parallélisation. Au delà
de ce problème, inexistant avec la plupart des VM récentes, nous avons voulu comparer
les performances de certaines VM en terme de rapidité d’exécution et d’efficacité de
parallélisation.
Deuxièmement, nous avons souhaité étudier plus en détail notre méthode de résolution
la plus élaborée - et la plus rapide : la méthode parallélisée dynamiquement. Cette
méthode utilise notre algorithme séquentiel le plus rapide, et est parallélisée sur un
nombre donné de threads suivant un système de producteur - consommateurs dans lequel les données échangées sont des sous-problèmes dans lesquels un certain nombre de
reines est déjà placé. On peut donc affiner cette méthode en jouant sur les 2 paramètres
« nombre de threads » et « taille des sous-problèmes ».
6.1 Comportement des différentes implémentations de la
machine virtuelle
6.1.1 Test du compteur multithreadé
Nous avons d’abord voulu comparer sur un test simple indépendant du problème des
N reines la rapidité et les capacités de parallélisation de 3 machines virtuelles :
– SUN 1.4.2 - version client
– SUN 1.4.2 - version serveur
– IBM 1.4.2
31
Notons que nous avions pensé à inclure dans ce panel de VM celle de GCC. Cependant,
ses performances étant terriblement plus mauvaises de tout point de vue, nous avons
préféré l’écarter afin de préserver des courbes lisibles.
Nous avons testé ces VM sur 2 machines physiques :
– infomob - Bi-Xeon 2.8 GHz (avec technologie Hyper-Threading)
– quadx1 - Quadri-Pentium III 700 MHz
Il sera également intéressant de comparer les capacités de parallélisation de ces 2 machines physiques, les 2 processeurs Xeon apparaissant sur infomob comme 4 processeurs
en raison de l’Hyper-Threading.
Le test utilisé est un simple compteur dont la tâche est de compter jusqu’à 100 000
000. Cette tâche peut être répartie sur un nombre de threads que l’on va faire varier de
1 à 10.
6.1.1.1 Protocole de mesure
Des aléas peuvent exister à cause de la possible activité du serveur lors des calculs, des
éventuelles différences de vitesse entre plusieurs exécutions du même test dues à la mise
en cache du programme, ou à cause du comportement plus au moins aléatoire de l’ordonnanceur du système d’exploitation. Pour minimiser leur impact, nous avons effectué
chaque mesure 10 fois de suite, en utilisant la fonction System.currentTimeMillis(). Sur
les courbes suivantes, on peut voir les points de données tels que nous les avons mesurés,
ainsi que 3 courbes qui en dérivent :
– La courbe de la moyenne
– La courbe de la moyenne, après avoir écarté pour chaque mesure la valeur maximale
– La courbe des minima
32
33
Sur les 2 exemples ci-dessus, on voit l’intérêt d’un tel traitement. La courbe de la
moyenne débarassée de la valeur maximale en chaque point est la plus fiable et c’est
pourquoi nous utiliserons celle-ci dans la suite de nos expérimentations. En effet, on
voit sur la première courbe qu’elle a permis de ne pas donner d’importance à des points
clairement aberrants. Sur la deuxième courbe, très régulière, elle reste cohérente avec les
points obtenus.
34
6.1.1.2 Comparaison des courbes de temps en fonction du nombre de threads
35
Ces courbes nous permettent de constater que la VM d’IBM est la plus rapide sur
quadx1 alors que c’est la VM serveur de SUN qui l’emporte d’un courte tête sur infomob. La VM cliente est logiquement plus lente car elle est optimisée pour les programmes courts n’utilisant pas intensément le processeur, et ne prend donc pas le temps
de précompiler le code puisque le gain que cela apporterait ensuite serait faible.
6.1.1.3 Speed-ups de niveau 1
On représente ici le rapport : « temps d’éxécution sur un seul thread » / « temps
d’éxécution pour le nombre de threads spécifié ». Dans un système idéal indéfiniment
parallélisable, ce rapport est une droite y = x. On sera en réalité limité bien sûr par le
nombre de processeurs mais aussi par les overheads.
36
37
On constate que cette fois-ci c’est la VM client de SUN qui s’en sort le mieux, suivie au
début de près par la VM serveur de SUN. La différence est particulièrement remarquable
sur infomob, où les 2 autres machines virtuelles ne parviennent pas à se paralléliser avec
efficacité. Sur quadx1, le résultat est le même bien que l’écart soit moins imposant.
Surpris par ce phénomène, nous nous sommes d’abord demandé s’il n’était pas dû à
une sous-évaluation des performances de la VM client pour un seul thread, mais les 10
mesures effectuées sont très régulières, ce qui écarte cette éventualité.
Nous ne sommes pas sûrs des raisons de cette supériorité de la VM client. Cependant,
nous nous permettrons d’avancer une hypothèse :
On constate que sur infomob, pour les VM IBM et SUN serveur, les performances
chutent avec l’introduction d’un troisième thread. La VM client a peut-être plus tendance à rendre spontanément la main à l’ordonnaceur, de sorte les trois threads sont
équitablement partagés et finissent presque tous en même temps. Les VM « serveurs »,
elles, si elles gardent longtemps la main, exécutent les 2 premiers threads d’une traite,
et se retrouvent à moitié au « chômage » lorsqu’il ne reste plus qu’un thread.
Il apparaı̂t le même phénomène sur quadx1 pour 5 threads, mais ici, étrangement,
toutes les VM sont touchées.
En tout cas, ceci n’explique pas non plus que sur infomob, la VM client puisse obtenir
des speed-ups légèrement supérieurs à 2 pour un nombre de threads plus important. Ceci
est peut-être dû à l’Hyper-Threading, ou à des gains de temps au niveau de l’éxécution
38
de threads qui partagent le même code et n’ont peut-être pas besoin d’être recompilés.
Sinon globalement les performances sont bonnes pour un nombre de threads égal au
nombre de processeurs : on obtient des speed-ups proches de 4 sur quadx1.
6.1.1.4 Speed-ups de niveau 2
On représente le rapport « meilleur temps d’éxécution sur un seul thread » / « temps
d’éxécution pour le nombre de threads spécifié ». On pourra ainsi visualiser si la meilleure
parallélisation d’une des VM de SUN lui permet de rattraper les performances de la VM
d’IBM sur quadx1.
39
On constate, comme plus haut bien sûr, que la VM d’IBM garde malgré tout son
avantage. On remarque aussi que quelle que soit la VM, le nombre de threads optimal
semble être le nombre de processeur de la machine, et cela sans que le fait que les
processeurs soient « Hyper-Threadés » n’ait une influence notable.
Sur infomob, SUN serveur et IBM se partagent les premières places, mais c’est SUN
qui est la plus performante avec le plus haut point du graphe pour 2 threads.
6.1.2 Utilisation réelle avec le problème des 14 reines
On utilise ici les mêmes machines virtuelles et physiques, ainsi que le même protcole
de mesure pour éviter les aberrations. Mais le test utilisé est maintenant la résolution
du problème des 14 reines par la méthode de résolution parallélisée dynamiquement.
Dernière minute : suite à des mesures aberrantes lors du test de la VM serveur sur
infomob, les graphes suivants contiennent des informations erronées.
40
6.1.2.1 Speed-ups de niveau 1
41
42
6.1.2.2 Speed-ups de niveau 2
43
On constate que la VM garde le monopole de la vitesse d’éxécution sur quadx1.
44
6.2 Paramètres optimaux pour la parallélisation dynamique
On voit apparaı̂tre sur ce graphe ou l’algorithme atteint des minimums de temps
d’éxécutions : celui qui correspond à 4 threads avec une profondeur de division de 1,
et celui à 2 threads avec une profondeur de division de 5. On ne peut pas se baser sur
telles mesures pour décider quels sont les meilleurs paramètres pour cette fonction sur
d’autre machine, et avec N<>15. Cependant on peut remarquer que le schéma « nombre
de threads = nombre de processeurs » reste une valeur sûre.
45
46
Conclusion
Du point de vue de notre cahier de charge, ce projet est objectivement une réussite.
Tous les objectifs principaux ont été atteints : nous disposons de plusieurs méthodes,
implémentant successivement des optimisations supplémentaires, et nous les avons parallélisée de manière à pouvoir profiter de manière satisfaisante des capacités de machines
multiprocesseurs. Nous avons pu aussi nous intéresser à une bonne partie des objectifs
secondaires. En particulier l’archivage des résultats des tests a pu être effectué et a été
l’occasion de nous familiariser avec la gestion des entrées / sorties en Java, ce qui nous fut
également utile pour générer automatiquement les fichiers textes qui nous ont permis de
tracer les courbes de ce rapport à partir des résultats de tests précédemment sérialisés.
Cependant, il reste encore beaucoup de choses qu’il pourrait être intéressant d’essayer.
Notamment, nous aurions bien voulu implémenter un algorithme travaillant sur les
permutations des N reines, ce qui nous aurait permis de faire un backtracking sur un
arbre dont chaque noeud a un fils de moins que son père, au lieu de le faire sur un
arbre dont chaque noeud à N fils. Il est à noter que le gain de temps d’un tel algorithme
ne sera sans doute pas immédiat, car d’une part, passer à la position suivante (qui
est l’opération la plus répétée de l’algorithme de backtracking) nécessite maintenant de
générer la permutation suivante, ce qui est un peu plus couteux (mais toujours en O(1)
) que de simplement incrémenter la dernière reine, et d’autre part, les positions que l’on
évite grâce à la génération de permutation sont des positions qui n’aurait pas donné lieu
à un sous-arbre de recherche. Le gain de temps ne se ferait donc sans doute sentir qu’à
partir d’un N assez grand. Il aurait donc été également instructif de mesurer si cette
méthode aurait été avantageuse.
Il aurait aussi été logique de continuer de developper l’interface graphique pour nous
permettre d’effectuer les opérations d’archivage par son intermédiaire. De même, l’interface console nécessiterait une petite amélioration du système d’aide qui n’indique pas
dans quel ordre passer les paramètres supplémentaires des méthodes.
Enfin, il serait également raisonnable de ne pas utiliser Java pour faire ce genre calcul intensif. Un passage sous un langage plus orienté vers la vitesse d’exécution serait
souhaitable.
47