Exploration d'un arbre
Le but de cet exercice est de vous montrer comment paralléliser l'exploration d'un arbre.
Pour cela, on implémentera un algorithme séquentiel avant de comparer différentes méthodes
de parallélisation. Nous utiliserons les bibliothèques rand pour générer des
chaines de caractères aléatoires, rayon pour du parallélisme avec
work-stealing et crossbeam pour du parallélisme avec threads classiques.
Vous pouvez donc ajouter les lignes suivantes à votre fichier Cargo.toml
.
crossbeam = "0.4"
rand = "0.5"
rayon = "1.0"
Algorithme séquentiel
On considère des arbres binaires dont les feuilles sont étiquetées par des chaînes de caractères. En pratique, cela donne le type ci-dessous. On remarquera que les fils d'un nœud sont boxés. Cela est obligatoire pour les types récursifs qui n'auraient sinon pas une taille fixe.
# #![allow(unused_variables)] #fn main() { pub enum Tree { Node(Box<Tree>, Box<Tree>), Leaf(String), } #}
Question 1. Écrivez une fonction count_ab_seq
qui compte le nombre feuilles
contenant le sous mot "ab". Vous testerez votre fonction sur de petits exemples d'arbres.
Évaluation de la performance
On veux tester notre code sur des arbres équilibrés de différentes façon, pour voire
comment les différentes méthodes de parallélisation se comportent dans chaque cas. Pour
cela, on donne une fonction gen_tree
qui génère un arbre binaire déséquilibré vers la
droite d'un facteur balance
. Si balance = 1
, l'arbre est équilibré.
# #![allow(unused_variables)] #fn main() { use rand::{XorShiftRng, Rng}; /// Génère un arbre avec `n` feuilles tel que chaque sous-arbre ayant plus de `balance` feuilles /// contiennent `balance` fois plus de feuilles dans son fils droit que gauche. pub fn gen_tree(n: usize, balance: usize, rng: &mut XorShiftRng) -> Tree { match n { 0 => panic!(), 1 => Tree::Leaf(rng.gen_ascii_chars().take(500).collect()), n if n <= balance => gen_tree(n, 1, rng), n => { let lhs = gen_tree(n/(balance+1), balance, rng); let rhs = gen_tree(n-n/(balance+1), balance, rng); Tree::Node(Box::new(lhs), Box::new(rhs)) }, } } /// Example de génération d'un arbre de 100 000 feuilles, déséquilibré d'un facteur 8. let tree = gen_tree(100_000, 8, &mut rand::weak_rng()); #}
Question 2. Écrivez des benchmarks pour la fonction count_ab_seq
sur:
- un arbre équilibré de 100 000 feuilles,
- un arbre déséquilibré de 100 000 feuilles et
- un arbre de 100 feuilles.
Afin d'utiliser les mêmes arbres pour tous les benchmarks, vous les déclarerez comme des variables statiques en utilisant la bibliothèque lazy static.
Parallélisme de threads (lourd)
On veux maintenant paralléliser notre code. Une première solution naïve est d'explorer les sous-arbres de chaque nœud en parallèle en créant un thread par fils.
Question 3. Écrivez un version parallèle count_ab_heavy_par
de la fonction
count_ab_seq
qui créé un thread pour chaque fils de chaque nœud. Pour créer les
threads, vous utiliserez la bibliothèque crossbeam et
notamment la fonction scope et la méthode
spawn. Nous vous invitons à lire attentivement la
documentation de cette méthode pour comprendre pourquoi elle est préférable,
dans notre cas, au spawn de la bibliothèque standard.
Question 4. Comparez la performance de count_ab_heavy_par
à count_ab_seq
sur
l'arbre de 100 feuilles. Que se passe-t-il si on lance count_ab_heavy_par
sur un arbre
de 100 000 feuilles ?
Parallélisme many-to-many statique
On essaye maintenant de réduire la perte de performance infligée par la création de trop de threads. Pour cela, on ne vas générer des threads que jusqu'à une profondeur fixée.
Question 5. Écrivez une fonction count_ab_fixed_par
qui se comporte comme
count_ab_heavy_par
jusqu'à une profondeur donnée (par exemple via un argument) puis
comme count_ab_seq
.
Question 6. Mesurer la performance de count_ab_fixed_par
sur un arbre équilibré et
un arbre déséquilibré. Comment expliquer la différence de performance par rapport à
count_ab_heavy_par
? Comment expliquer la différence de performance entre les arbres
équilibrés et déséquilibrés ?
Parallélisme many-to-many dynamique
On va maintenant comparer les deux méthodes précédentes à une parallélisation utilisant du work-stealing. Pour cela, on utilisera la fonction join de rayon.
Question 7. Parallélisez la fonction count_ab_seq
en utilisant rayon::join
.
Comparez aux autres méthodes de parallélisation et expliquez les différences de
performance.