| August 13, 2009 10:00 PM PDT | |
Version PDF
Cet article est également disponible en téléchargement [PDF de 78 ko].
Introduction
Dans le cadre de la parallélisation des logiciels pour stimuler leur rapidité d’exécution sur les plates-formes Intel® multicœurs, les bibliothèques logicielles jouent un rôle déterminant.
Or ces plates-formes sont aujourd’hui le standard pour les micro-ordinateurs vendus dans le monde entier. Rappelons qu’elles sont dotées d’un processeur dont la caractéristique est de regrouper, sur une même puce, plusieurs cœurs d’exécution, qui sont en fait autant de processeurs à part entière. Il y a dix ans, seuls les serveurs et les stations de travail haut de gamme disposaient de plusieurs processeurs (elles étaient donc « multiprocesseurs » et pratiquaient ce que l’on appelle le « traitement SMP » [Symmetric Multi-processing]). Un PC de bureau ou portable embarquant une plate-forme multicœur dégage ainsi aujourd’hui des performances équivalentes à celle de ces anciennes configurations multiprocesseurs. (A noter que les serveurs et stations de travail haut de gamme sont quant à elles équipées désormais de plates-formes multiprocesseurs et multicœurs.) Chacun des cœurs d’un processeur multicœur est capable d’effectuer des tâches de traitement indépendamment des autres, soit une augmentation des performances au niveau de la plate-forme et un meilleur confort d’utilisation.
Le multicœur, cependant, a une incidence sur les logiciels, qui, pour exploiter pleinement les ressources de traitement additionnelles qu’il implique, nécessitent d’être optimisés à cet effet : ils doivent être parallélisés (threaded en anglais). Or il existe une méthode de développement peu coûteuse pour ce faire : le recours à des bibliothèques (libraries) déjà parallélisées. Cet article aborde ainsi le rôle et les avantages des bibliothèques pour la réalisation de logiciels à destination de plates-formes multicœurs.
Afin d’exploiter la présence de plusieurs cœurs de traitement, un logiciel doit avoir été conçu pour répartir la charge logicielle entre ceux-ci. Autrement dit, il doit se composer de fils d’exécution (threads), qui sont autant d’unités d’exécution dont la particularité est de pouvoir se traiter en parallèle (d’où la notion de parallélisation), chacun sur un cœur. La parallélisation d’un logiciel représente ainsi la méthode la plus courante pour assurer la possibilité d’une exécution concurrente. Comme les fils font partie d’un même processus, ils peuvent donc partager les données concernées. Chacun reprend une ou plusieurs tâches, dont le système d’exploitation confie le traitement à l’un des cœurs du processeur. En fait, les applications d’infrastructure destinées aux serveurs (multiprocesseurs) sont parallélisées depuis longtemps et sont donc en mesure d’exploiter le multicœur. En revanche, s’il est vrai qu’une grande partie des logiciels micro-informatiques est elle aussi déjà parallélisée, cette démarche ne procède dans ce cas que d’une recherche de commodité de programmation : seules les tâches qui ne sont pas lourdes en traitement sont affectées à des fils différents (E/S, par exemple). Or, dans ce cas, les gains de performances obtenus sur plates-formes multicœurs ne sont en général qu’assez faibles. En revanche, si les tâches affectées à ces fils sont lourdes en traitement, le fait qu’elles puissent se réaliser de manière indépendante et concurrente augmente les performances globales de l’application concernée. La répartition de tâches applicatives lourdes entre plusieurs cœurs représente ainsi un bénéfice concret pour l’utilisateur, qui correspond à l’un des avantages suivants :
- l’application est plus rapide et l’utilisateur abat un maximum de travail en un minimum de temps ;
- l’utilisateur bénéficie d’une amélioration des performances d’affichage ou sonores, sous la forme, par exemple, d’images en plus forte résolution ou bien, pour les jeux, de scènes d’un rendu de meilleure qualité ou de la possibilité d’afficher un plus grand nombre d’objets en même temps.
Parallélisation et bibliothèques
Les bibliothèques constituent un outil très précieux pour le développement logiciel. Réalisées en interne ou bien utilisées sous licence, elles peuvent en effet jouer un rôle important dans le codage des logiciels ainsi que pour stimuler les performances de ceux-ci au travers de leur parallélisation. Elles représentent en particulier un excellent moyen pour réutiliser du code déjà écrit, ce qui se traduit par des économies sur les coûts de développement et de maintenance. Une bibliothèque multithread peut servir à de nombreuses applications. Après s’être familiarisé avec les bibliothèques, on peut ainsi les utiliser pour différents projets. La difficulté, en revanche, consiste à choisir la plus adaptée à un projet, car les critères de choix sont nombreux. Cet article se limite ainsi aux critères liés à la parallélisation.
Protection contre les accès concurrents
Une bibliothèque peut être « threadsafe », « multithread » ou les deux. Dans le cas d’une bibliothèque threadsafe, plusieurs fils (threads) d’une application peuvent y appeler des fonctions. La difficulté la plus fréquente pour rendre une bibliothèque threadsafe est l’écueil que représentent les variables statiques et globales. Si cette bibliothèque ou ses fonctions comportent des variables statiques et que plusieurs fils les lisent et les actualisent, cette situation peut en effet déboucher sur des comportements indéfinis, et il en va de même pour les variables globales, qu’elles soient externes ou internes à la bibliothèque. On rend une bibliothèque threadsafe par l’adjonction de mutex (du mot-valise anglais formé à partir de la locution mutual exclusion) ou de sections critiques au code qui accèdent aux variables statiques ou globales. Bien que cette opération puisse se révéler nécessaire, dans un souci de performances, il vaut cependant mieux, cependant, limiter le nombre de ces mutex et sections critiques. A chaque fois que possible, il est ainsi préférable d’effectuer une copie locale des données et de les transmettre à la bibliothèque sous la forme d’un paramètre. Chaque fil disposant ainsi de sa propre copie locale, aucun ne rencontre de problème de course de données (data race) en effectuant les tâches qui lui sont affectées.
Parallélisation des bibliothèques
Lorsqu’une bibliothèque est « parallélisée », c’est qu’elle décompose ses tâches en unités plus petites, dont elle confie la réalisation à plusieurs threads. Là encore, il peut se révéler nécessaire, pour éviter les accès concurrents, de recourir à des mutex ou à des sections critiques. En général, une bibliothèque crée son propre jeu de fils pour le traitement, mais il est néanmoins possible qu’elle utilise un « pool » de fils partagés avec l’application principale, ce qui nécessite alors plus de précautions et une conception plus réfléchie. Le fait que la bibliothèque et l’application principale utilisent le même pool de fils peut en revanche rationaliser la programmation. Nous avons ainsi vu des cas où l’équipe de développement d’un éditeur de logiciel avait créé des fils pour chaque bibliothèque pour parvenir ainsi à plus de quatre-vingt et threads, sur lesquels seuls quatre étaient actifs à un instant T. Un pool de fil peut permettre d’éviter ce fractionnement inutile des ressources.
Parallélisation des bibliothèques et protection contre les accès concurrents
Une bibliothèque parallélisée peut être threadsafe ou ne pas l’être. Si elle est à la fois parallélisée et threadsafe, cela signifie que plusieurs fils peuvent y accéder en même temps et qu’elle peut s’embrancher en d’autres fils, qui s’exécuteront tous en parallèle. C’est ce que l’on appelle une « parallélisation imbriquée » (nested threading). Si la bibliothèque en question est parallélisée mais non threadsafe, les fils ne doivent y accéder qu’à raison d’un seul à la fois : dans le cas contraire, c’est-à-dire si plusieurs fils y accèdent en même temps, le comportement de l’application risque d’être indéfini, et les résultats non déterministes. Il convient donc de demander à l’éditeur ou au développeur d’une bibliothèque dont on envisage l’utilisation de fournir de la documentation sur son exploitation en environnements parallélisés. En outre, l’outil Intel Thread Checker peut lui aussi permettre de vérifier qu’une bibliothèque est threadsafe.
Par ailleurs, une bibliothèque ou ses fonctions peuvent être threadsafe, mais aussi dépendre des pointeurs qui leurs sont transmis. Ainsi, la commande stringcopy de la bibliothèque d’exécution (runtime library) peut être threadsafe et on peut l’appeler sans problème plusieurs fois à partir de plusieurs fils. En revanche, si cette commande est appelée deux fois à partir de fils distincts et que la même adresse cible est transmise pour les deux appels, le résultat ne sera pas déterministe, non parce que la fonction concernée n’est pas threadsafe, mais parce qu’elle n’a pas été appelée correctement.
Exemples d’utilisation de bibliothèques parallélisées
Les bibliothèques Intel MKL (Math Kernel Libraries) sont à la fois parallélisées et threadsafe. Le graphe ci-dessous illustre leur capacité de dimensionnement face à l’augmentation du nombre de fils (d’un à huit). Elles intègrent plusieurs interfaces API différentes à la routine FFT 3D, ce qui permet aux développeurs de les adopter facilement. Le graphe ci-dessous montre qu’elles sont déjà optimisées, ce qui améliore les performances d’un code analytique nécessitant une transformée de Fourier rapide (FFT) 3D.
Création d’une bibliothèque parallélisée ou threadsafe
Il est conseillé aux développeurs qui optent pour la création, en interne, de leur propre bibliothèque multithread ou threadsafe d’envisager une procédure que nous avons déjà évoquée à de nombreuses reprises (cf. schéma représenté plus loin). Celle-ci se décompose en quatre phases :
- Analyse
- Sélection et implémentation
- Vérification
- Optimisation
Analyse. La première étape est celle de l’analyse. Lorsque l’on entame un nouveau projet, l’outil d’analyse est souvent un tableau blanc ou un logiciel de modélisation UML. On établit alors l’enchaînement des activités et le flux des données. C’est dès ce stade qu’il faut envisager les possibilités de parallélisation, en déterminant les tâches qui peuvent s’effectuer en parallèle ainsi que celles qui peuvent se décomposer en sous-tâches dont l’exécution sera confiée à plusieurs fils traités en simultané. La parallélisation est à prévoir dans l’architecture du logiciel dès les tout débuts. C’est en effet le meilleur moyen pour obtenir un code facilement dimensionnable. Lorsque l’on travaille sur du code existant, comme c’est souvent le cas, il est alors important de bien comprendre son comportement, à tel point que même si l’on estime maîtriser ce comportement, mieux vaut le vérifier concrètement. Nous conseillons à ce titre le recours à l’analyseur de performances VTune™, pour appréhender les goulets de performances et la collecte des données afin d’évaluer l’intérêt qu’il y a à paralléliser une bibliothèque ou une zone de code donnée.
Sélection et implémentation. Après avoir terminé l’analyse et déterminé une zone dont on souhaite améliorer les performances, il faut choisir le modèle de parallélisation le plus approprié. S’il s’agit de rendre une bibliothèque threadsafe, on pourra faire l’impasse sur ce choix. S’il s’agit au contraire de la paralléliser, il existe plusieurs options. Nous — entendez Intel — ne saurions trop vous conseiller de faire appel à une abstraction de la parallélisation, qui permettra de repérer les tâches parallélisables et de faire gérer les fils correspondants par une bibliothèque d’exécution, à choisir de préférence entre les bibliothèques Intel Threading Building Blocks et OpenMP*. La première correspond à une parallélisation par recours à des gabarits (templates) C++, tandis que la seconde fait appel à des pragmas. (A noter qu’OpenMP est incluse dans les compilateurs Intel ainsi que dans de nombreux autres compilateurs courants.) La dernière méthode, qui est aussi la plus laborieuse et la plus faillible, consiste à employer des fils « bruts », autrement dit des fils Windows* ou POSIX*. (On trouvera dans les références, en fin d’article, des liens vers des supports qui approfondissent la parallélisation, le débogage et l’optimisation).
Vérification. Qu’il s’agisse de paralléliser une bibliothèque ou de la rendre threadsafe, l’outil Intel Thread Checker est un instrument quasi indispensable pour repérer les erreurs de parallélisation les plus courantes telles que les courses de données. Si la bibliothèque concernée doit être threadsafe, il faudra écrire un petit programme pilote qui fera appeler celle-ci en même temps par plusieurs fils ; s’il s’agit au contraire de la paralléliser, on utilisera cet outil sur le programme, à l’aide d’une charge applicative qui appelle la bibliothèque. Dans les deux cas, Intel Thread Checker signalera le numéro des lignes du code source responsables d’une course de données. On peut également se servir de cet outil pour vérifier que des bibliothèques obtenues en externes sont threadsafe. On écrira alors un programme pilote dont plusieurs fils appellent en même temps la bibliothèque et on vérifiera ensuite à l’aide de l’outil. Lorsque le code source n’est pas disponible, cet utilitaire signale le désassemblage et déroule la pile d’appel des fonctions.
Optimisation (tuning). Lorsque l’application fonctionne correctement, l’étape suivante consiste a priori à optimiser la bibliothèque à présent parallélisée ou bien le code qui appelle la bibliothèque threadsafe. L’outil Intel Thread Profiler est à ce titre un instrument de stimulation des performances qui permet de repérer les déséquilibres dans les charges logicielles et les appels de synchronisation trop fréquents, ceux-ci représentant deux freins aux performances. Le temps que l’on veut bien consacrer à cette étape se traduit souvent par des gains de performances tout à fait significatifs.
Résumé
Les plates-formes multicœurs se généralisent et le nombre de cœurs de traitement qu’elles regroupent suit une progression géométrique. Les logiciels parallélisés dans l’optique des performances ont donc un avantage sur leurs concurrents qui ne bénéficient pas d’une telle parallélisation. L’élaboration ou l’utilisation sous licence de bibliothèques multithread ou threadsafe et réutilisables pour des lignes de produits différentes constitue un excellent moyen d’exploiter le multicœur.
AutoCAD* 2007 fait partie de ces logiciels précurseurs et assure à ses utilisateurs d’excellentes performances. Les développeurs ont donc tout intérêt à penser leurs logiciels pour cette architecture et, dans cette optique, à faire appel aux bibliothèques, parce qu’elles représentent un moyen rationnel d’y parvenir. Elles permettent en effet d’accélérer la parallélisation d’un logiciel en développement et de renforcer les performances de celui-ci. Les outils Intel pour le génie logiciel permettent par ailleurs de déterminer plus facilement les zones de code à paralléliser, elles proposent d’excellents modèles d’abstraction de la parallélisation et permettent de repérer ainsi que d’éliminer les sources de courses de données et les goulets de performances. Tous ces outils sont disponibles en version d’évaluation de trente jours à partir de la page qui leur est consacrée.
Références
- Kirkegaard, Knud J. et al., « Methodology, Tools and Techniques to Parallelize Large-Scale Applications: A Case Study », Intel Technology Journal, volume 11, n° 04 (http://www.intel.com/technology/itj/2007/v11i4/6-tools/1-abstract.htm).
- « Threading Methodology: Principles and Practices », version 2.0.
Reportez-vous à notre Notice d'optimisation pour plus d'informations sur les choix et l'optimisation des performances dans les produits logiciels Intel.
Commentaires (0) 
Trackbacks (0)
Réagir 
Day Kol (Intel)
|
