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