Améliorer le parcours d'un arbre binaire avec le traitement en parallèle
Apprends à améliorer les sommes d'arbres binaires en utilisant des techniques de programmation parallèle.
― 6 min lire
Table des matières
La programmation parallèle nous permet de mieux utiliser les ressources des ordinateurs en exécutant plusieurs tâches en même temps. C'est super utile quand on bosse avec de gros ensembles de données ou des calculs complexes. Dans cet article, on va voir comment améliorer un algorithme spécifique pour parcourir un Arbre binaire et profiter des multiples cœurs d'un ordinateur.
Parcours d'arbre
Le défi duParcourir un arbre, c'est visiter chaque nœud et effectuer une opération, comme calculer la somme des valeurs dans chaque nœud. Toutefois, les arbres peuvent avoir des structures très différentes. Certains arbres sont bien équilibrés, ce qui facilite le Traitement parallèle, tandis que d'autres peuvent être déséquilibrés ou petits. Cette variabilité peut poser des défis quant à l'efficacité de l'utilisation du traitement parallèle.
Pour cet exemple, on va voir comment on peut faire la somme des valeurs d'un arbre binaire en utilisant le traitement parallèle. L'objectif est de créer une solution qui fonctionne bien indépendamment de la structure de l'arbre.
L'algorithme de base
Le point de départ de notre discussion est un algorithme de base pour faire la somme des valeurs dans un arbre binaire. On vérifie si le nœud actuel est nul ; si c'est le cas, on retourne 0. Si ce n'est pas nul, on continue en additionnant les valeurs des branches gauche et droite avec la valeur du nœud courant.
int sum(node* n) {
if (n == nullptr) return 0;
return sum(n->left) + sum(n->right) + n->value;
}
Introduction au parallélisme
Pour profiter des multiples cœurs, on peut modifier cet algorithme en utilisant une technique appelée fork-join. Cette approche divise la charge de travail en tâches plus petites qui peuvent s'exécuter en parallèle. Pour un arbre binaire, cela veut dire qu'on peut faire la somme des branches gauche et droite en même temps.
On va créer un tableau pour stocker les résultats de la somme de chaque branche et lancer des tâches pour chaque branche. Quand les tâches sont terminées, on combine leurs résultats.
int sum(node* n) {
if (n == nullptr) return 0;
int results[2];
fork_join(() -> results[0] = sum(n->left),
() -> results[1] = sum(n->right));
return results[0] + results[1] + n->value;
}
Gérer la variabilité des entrées
Le défi reste que l'arbre peut être très différent en structure. Par exemple, il pourrait être une longue chaîne ou un très petit arbre. Dans ces cas, on pourrait ne pas avoir assez de travail pour justifier le traitement parallèle, ce qui peut mener à du gaspillage de ressources.
Pour résoudre ça, on doit contrôler la granularité de nos tâches – c'est-à-dire gérer la charge de travail de chaque tâche. On va utiliser une technique appelée planification par battement pour contrôler la bascule entre le traitement sériel et parallèle.
Planification par battement
La planification par battement consiste à alterner entre le traitement sériel et parallèle en fonction d'un compte de "battement" défini. On va effectuer certaines opérations en série, puis après un certain nombre d'opérations, passer au parallèle. De cette façon, on peut mieux gérer les différentes structures d'arbres tout en s'assurant que le système reste efficace.
Le compte de battement aide à décider quand changer de mode. On crée une fonction d'assistance qui indique s'il est temps de changer.
bool heartbeat() {
// Retourne vrai toutes les N appels pour changer de mode
}
Promotion des tâches
Pendant le parcours, on peut chercher des occasions de promouvoir des tâches sérielles en parallèles – c'est-à-dire quand on trouve un point où on pourrait exécuter deux tâches à la fois. Par exemple, si on est en train de faire la somme de la branche gauche et qu'on n'a pas encore commencé celle de la droite, on peut créer une nouvelle tâche pour la branche droite.
La promotion nous aide à exploiter des tâches parallèles latentes qui peuvent ne pas être encore en cours d'exécution mais peuvent être activées dès que les conditions le permettent. C'est important pour éviter le temps d'inactivité pendant le traitement.
void try_promote(kont* k) {
// Vérifie les opportunités de promouvoir une tâche
}
Fusionner les approches
La dernière étape consiste à fusionner les approches sérielles et parallèles que nous avons construites. En alternant entre les méthodes et en promouvant les tâches lorsque c'est possible, on crée un algorithme de parcours d'arbre flexible qui s'adapte à la structure des données qu'il traite.
La fusion prend le meilleur des deux mondes – exécuter les tâches en parallèle quand c'est pertinent et revenir à une méthode plus lente mais sûre quand c'est nécessaire. Cette adaptabilité est essentielle pour gérer la nature variée des arbres que nous voulons parcourir.
int sum(node* n, kont* k) {
while (true) {
k = try_promote(k);
if (heartbeat()) {
// Passer à l'autre méthode de traitement
}
if (n == nullptr) return 0;
// Continuer le traitement...
}
}
Évaluation des performances
Pour tester notre nouvelle approche, on peut l'évaluer sur différentes structures d'arbres. Par exemple, on peut comparer les performances sur un arbre parfaitement équilibré contre une chaîne ou un arbre aléatoire. L'objectif est de voir si notre implémentation peut gérer efficacement différents scénarios et offrir des améliorations de vitesse par rapport à la méthode sérielle originale.
En collectant des données de performance, on peut mieux comprendre l'impact de nos optimisations et où elles brillent le plus. On peut aussi faire des benchmarks par rapport à des solutions existantes pour voir comment notre travail se positionne.
Conclusion
En conclusion, le chemin pour refactoriser un algorithme de parcours d'arbre binaire afin qu'il fonctionne efficacement sur plusieurs cœurs a montré la valeur de combiner le traitement parallèle et sériel. En utilisant la planification par battement et la promotion des tâches, on peut construire un algorithme qui s'adapte bien à la structure de l'arbre.
Cette méthode améliore non seulement la performance mais facilite également la gestion de diverses structures d'arbres dans des applications réelles. À mesure que la puissance de calcul continue de croître, de telles techniques seront essentielles pour une programmation efficace.
Dans l'ensemble, les améliorations discutées offrent un moyen de maximiser les ressources à notre disposition, s'assurant qu'on peut traiter efficacement les problèmes computationnels tout en maintenant une structure claire dans notre code.
Titre: The best multicore-parallelization refactoring you've never heard of
Résumé: In this short paper, we explore a new way to refactor a simple but tricky-to-parallelize tree-traversal algorithm to harness multicore parallelism. Crucially, the refactoring draws from some classic techniques from programming-languages research, such as the continuation-passing-style transform and defunctionalization. The algorithm we consider faces a particularly acute granularity-control challenge, owing to the wide range of inputs it has to deal with. Our solution achieves efficiency from heartbeat scheduling, a recent approach to automatic granularity control. We present our solution in a series of individually simple refactoring steps, starting from a high-level, recursive specification of the algorithm. As such, our approach may prove useful as a teaching tool, and perhaps be used for one-off parallelizations, as the technique requires no special compiler support.
Auteurs: Mike Rainey
Dernière mise à jour: 2023-07-19 00:00:00
Langue: English
Source URL: https://arxiv.org/abs/2307.10556
Source PDF: https://arxiv.org/pdf/2307.10556
Licence: https://creativecommons.org/licenses/by/4.0/
Changements: Ce résumé a été créé avec l'aide de l'IA et peut contenir des inexactitudes. Pour obtenir des informations précises, veuillez vous référer aux documents sources originaux dont les liens figurent ici.
Merci à arxiv pour l'utilisation de son interopérabilité en libre accès.