Threading Building Block est une librairie qui a pour but d'offrir une interface agréable pour les développeurs en C++ qui souhaitent paralléliser leur code. Agréable, car vous avez la liberté de rester à un haut niveau d'abstraction, garder une formulation objet fonctionnelle et de ne pas vous intéresser aux détails d'implémentation. Le croirez-vous, modélisation objet et performance ne sont cette fois-ci pas incompatibles, bien au contraire.
Le fait de définir avec précision les données du problème en mode objet sans entrer dans les détails techniques donne à la librairie tout ce dont elle a besoin pour être performante sans lui imposer un cadre trop strict. Dans d'autres systèmes comme OpenMP vous ne définissez (pour simplifier) que votre modèle de données, la librairie n'a donc pas autant d'informations. La programmation fonctionnelle vous permettra elle de définir des architectures logicielles naturellement adaptées au multi-core. Vous pouvez être au summum, à la fois de la performance et de la modélisation, pourquoi se priver ?
EXEMPLE SIMPLEEssayons de faire nos premiers pas parallèles avec TBB en commençant par un cas simple. Dans les exemples, vous trouverez "GettingStarted/sub_string_finder". Si vous êtes en ligne de commande il suffit de dézipper les sources et en-têtes et taper "make". Des projets pour Visual Studio et Xcode sont aussi disponibles. Il s'agit d'un problème simple de traitement de chaînes : pour chaque position dans une chaîne, savoir la longueur de la sous-chaîne qui se retrouve ailleurs dans la chaîne principale. Le problème n'est pas fondamental mais le code est simple à comprendre et à paralléliser. TBB vous évite certes d'avoir à coder les détails d'implémentation, mais il vous reste tout de même à définir à quel niveau de votre algorithme vous souhaitez inclure du parallélisme. Nous observons que l'algorithme peut être parallélisé au niveau du "pour chaque position dans une chaîne". En effet, chaque analyse peut s'effectuer sans connaissance du résultat des autres, tout ce qui est demandé est que chaque unité de traitement ait une copie de la chaîne en entrée et puisse agréger les résultats en sortie.
PROGRAMMATION FONCTIONNELLEIl faut tout d'abord déplacer votre code de calcul dans une classe, ici "SubStringFinder". La définition d'une classe permet d'avoir une vision claire des variables qui sont utilisées dans la partie parallèle de votre code. Ce point est un grand classique de la programmation parallèle. L'opérateur de la classe est lui la partie "calcul". Constructeur :
SubStringFinder(string &s, size_t *m, size_t *p) :
str(s), max_array(m), pos_array(p) { }Nous avons donc les données d'entrée (une chaîne) et une classe de calcul, il ne nous reste plus qu'à les associer pour lancer le traitement. Habituellement nous ferions directement appel à la classe en donnant une partie de la chaîne comme argument, le tout à partir d'une boucle pour couvrir toute la chaîne. Avec TBB je remplace la boucle par un appel à un "parallel_for" associé a un "blocked_range". La fonction lance le calcul, et le "range" est l'équivalent des limites de la boucle, avec des subtilités. La gestion de la classe de calcul reste identique. Appel TBB :
parallel_for(blocked_range
SubStringFinder( to_scan, max, pos ) );
Du point de vue du développeur, rien de révolutionnaire, il s'agit juste de reformuler dans un style de programmation fonctionnelle. La formulation est abstraite et simple à utiliser. Pour la librairie, par contre, cela change tout : TBB a maintenant le contrôle total du lancement de la partie parallèle d'une part, une partie calcul bien isolée et des données présentées sous une forme bien détaillée d'autre part. Le style n'est pas accessoire, il est à la base du fonctionnement de TBB.
AUTOMAGIQUEQuelques changements stylistiques, une recompilation, et voila que votre code s'exécute en parallèle. Magique non ? En pratique la librairie a d'abord créé un pool de threads adapté à l'environnement d'exécution et matériel. Il a ensuite fallu gérer les allocations mémoire nécessaires localement pour chaque thread et transférer les données nécessaires vers chaque thread. Puis gérer une queue de travail ou les différentes classes de calcul puisent leurs données. Enfin, rapatrier les résultats dans une variable unique du thread principal avant de rendre la main à la partie non parallèle de votre code.
VARIATIONSIl est toujours possible que chaque calcul prenne un temps différent en fonction de la donnée qui lui est fournie en entrée, ou qu'il y ait une granularité optimale pour découper vos données. Ces imperfections de parallélisme sont nuisibles à la sociabilité de votre logiciel parallèle, c'est-à-dire sa capacité à utiliser un grand nombre de coeurs efficacement. Si vous savez le prédire, renseignez TBB, le temps d'exécution n'en sera qu'amélioré. Ici nous avons opté pour un "blocked_range" pour distribuer en morceaux identiques la charge de travail entre threads, mais testez d'autres systèmes de répartitions. Idem pour la granularité. Il ne suffit après tout que d'un changement minime dans votre code et d'une recompilation.
RÉDUCTIONSNous venons de voir un cas simple ou les calculs exécutés en parallèle sont indépendants et où le résultat final n'est que l'agglomération des résultats unitaires. Prenons un exemple plus complexe pour comprendre le réel apport de TBB : calculons combien de nombres premiers se trouvent dans un intervalle en utilisant le crible d'Ératosthène (fichiers dans "parallel_reduce/primes"). Savoir si chaque nombre est premier est un calcul indépendant, mais le nombre final est lui une valeur commune. Il faut calculer une opération à la fin des blocs parallèles pour obtenir le résultat composite. La fonction TBB parallel_reduce va donc faire un appel à une fonction de jonction en plus de l'operateur de calcul dans votre classe. Ici la classe "Sieve" (crible en anglais) a donc une fonction "join" en plus d'"operator".
DIVISION ET CONQUÊTERien de bien spectaculaire pour les habitués de la programmation parallèle, la réduction est un classique. Mais la méthode par crible d'Ératosthène provoque un fort déséquilibre dans les calculs, qui plus est, dur à prévoir. L'implémentation qui vous est proposée utilise une méthode de division et conquête. En pratique, si un intervalle de nombres s'avère trop lourd à calculer pour un seul thread il est possible de le diviser entre plusieurs, dynamiquement au cours de l'exécution. Notez le second constructeur pour "Sieve", il est utilisé lors d'une division de tâche. Une réponse simple et stylée à un problème complexe. Vous voyez alors l'apport de la programmation fonctionnelle et comment TBB vous permet de l'utiliser pour paralléliser efficacement. TBB n'est pas seulement une énième interface vers la même couche de parallélisation mais bien un cadre qui fait le lien entre les dernières innovations en architecture logicielle et matérielle.
ET BIEN PLUS ENCORENous avons vu un exemple de parallel_for et parallel_reduce mais TBB ne se limite pas à ces deux cadres. Vous pouvez par exemple travailler sous forme de pipelines linéaires, c'est à dire définir des dépendances entre opérations et un degré de traitement parallèle. TBB fera le reste et gérera l'équilibre entre différentes phases de votre traitement. L'appel à parallel_do permet, lui, de gérer les cas ou le nombre d'itérations est inconnu à l'avance tout en maximisant la capacité à travailler en parallèle. Outre ces cadres de parallélisations, TBB vous propose des structures de données utilisables en parallèle. Des HashMap Vector et Queue accessibles en parallèle en toute sécurité ? C'est maintenant possible, même si vous utilisez des threads sans passer par TBB dans votre code. Vous pouviez déjà synchroniser et protéger tous les accès à ces structures pour assurer la validité de vos données, mais les structures de TBB le font avec un minimum de dégradation de performance.
TÂCHESAvoir une librairie qui cache la complexité des threads logiques est utile, mais les threads ne sont pas toujours le bon outil pour paralléliser. Avec certains algorithmes il est nécessaire d'avoir beaucoup plus d'unités de calcul que de coeurs disponibles. Si vous programmez un jeu vidéo ou chaque personnage voit son intelligence artificielle gérée par un thread, vous risquez d'avoir des problèmes pour un grand nombre de personnages. Alors que si vous utilisez des tâches, elles se partageront plus aisément les coeurs. La ou le gestionnaire de l'OS gérait la priorité entre threads, c'est TBB qui la gère entre tâches. Or TBB a plus d'informations sur les besoins réels des tâches et peut donc repartir de manière optimale. La tâche est donc l'outil rêvé pour programmer en fonctionnel sans tenir compte des ressources matérielles tout en les exploitant à leur optimum. J'espère que vous saurez maintenant mieux situer TBB dans l'éventail des technologies logicielles de parallélisations. TBB offre un rare potentiel de performance à ceux qui maîtrisent la programmation fonctionnelle (attention c'est addictif).
Paul Guermonprez
Sr. Software Engineer - Intel Software EMEA
Actualités
Intel Parallel Studio 2011 Getting Started
Pratique
Supplément C++
Livre blanc
Intel Parallel Studio 2011
Intel propose une nouvelle gamme d'outils de développement d'applications pour profiter du multicore via une programmation parallèle. Il s'agit de l'Intel® Parallel Studio qui d'adresse aux développeurs Windows qui possèdent Visual Studio* C/C++2008. Ce nouvel outil est en fait un ensemble qui réunit , Parallel Advisor pour diagnostiquer le code quant aux possibilités de parallélisation, Parallel Composer pour incorporer du parallélisme à l'aide d'un compilateur C++ Intel et des bibliothèques thread safe, Parallel Inspector pour découvrir des conflits de threading éventuels et Parallel Amplifier pour diagnostiquer le comportement des threads.
Liens
Actu
Pratique
Actu
Distributeurs INTEL