R1 · C++ pour le calcul¶
Le langage dans lequel vous écrirez tout le reste.
| Volume | ⏱ 45 h |
| Quand | L'été avant le semestre 3, ou en parallèle du S3 |
| Prérequis | MEIM11 · pointeurs, mémoire, compilation |
| Ce que la maquette FISA donne déjà | MOOB11 (modélisation objet, S1) : la conception, pas le C++ |
| Pourquoi c'est indispensable | PRPA23, PGPU35, COAV35 et PDSP35 exigent tous C ou C++ dans leurs prérequis |
| Livrable | Le projet en fin de module, dans votre dépôt de mesures |
D'où vient ce module
Son programme est celui de LAOA23 · Langages objet avancés, enseigné en deuxième année dans la formation sous statut étudiant. Aucune UE de la maquette FISA ne le couvre. Il est transposé ici en programme de travail personnel, débarrassé de la partie Qt, qui n'a aucun intérêt pour le calcul.
Ce que ça vaut pour le HPC¶
Moyen, mais indispensable. Il faut séparer les deux moitiés.
Ce qui compte : la STL et le C++ générique¶
Le découplage conteneurs/algorithmes par itérateurs est l'idée centrale du C++ et elle a une portée bien au-delà du confort d'écriture : c'est le même mécanisme qui permet aux bibliothèques HPC modernes — Kokkos, RAJA, Thrust, SYCL, la bibliothèque standard parallèle du C++17 — d'exprimer un calcul une seule fois et de le compiler pour CPU, pour GPU ou pour un autre accélérateur.
Autrement dit : ces concepts sont exactement ceux qui permettent d'écrire du code portable en performance, qui est le grand enjeu du HPC depuis que les machines sont hétérogènes.
Les foncteurs et, dans leur version moderne, les lambdas sont le
mécanisme par lequel on passe un noyau de calcul à un moteur d'exécution. Un
parallel_for de Kokkos prend une lambda ; un thrust::transform prend un
foncteur. Comprendre pourquoi un foncteur est plus rapide qu'un pointeur de
fonction — parce qu'il est inliné, alors que l'appel indirect ne l'est pas —
est une compétence de performance.
Arriver prêt¶
⏱ 20 h. C'est la mise en route du module, et elle compte : aucune UE de la maquette FISA n'enseigne le C++, et MEIM11 le partage avec Java.
- Bjarne Stroustrup, A Tour of C++, 3ᵉ éd. — lire les chapitres sur les modules, les templates, les concepts, les conteneurs et les algorithmes. ⏱ 15 h.
- S'habituer à
cppreference.complutôt qu'à des tutoriels. La page destd::vectorou destd::transformest la source de vérité. - Mettre en place le banc de mesure : CMake, Google Benchmark, et le réflexe Compiler Explorer. Tout le module se juge au chronomètre. ⏱ 3 h.
Ressources¶
Priorité 1 — le C++ utile¶
- Bjarne Stroustrup, A Tour of C++, 3ᵉ éd., Addison-Wesley, 2022. Le point d'entrée pour quelqu'un qui sait déjà programmer.
- Scott Meyers, Effective Modern C++, O'Reilly, 2014. Quarante-deux
conseils sur C++11 et C++14 : sémantique de déplacement,
auto, pointeurs intelligents, lambdas. Un peu daté sur les standards récents mais le contenu reste juste et c'est le meilleur livre pour passer du C++ « qui marche » au C++ « correct ». - Nicolai Josuttis, The C++ Standard Library: A Tutorial and Reference, 2ᵉ éd. La référence sur la STL. Épais ; à consulter, pas à lire.
- La C++ Core Guidelines (Stroustrup et Sutter, en ligne sur
isocpp.github.io/CppCoreGuidelines/). Gratuit, dense, faisant autorité.
Priorité 2 — la performance en C++¶
- Les conférences de la CppCon, sur YouTube. Trois en particulier, pour un
profil HPC :
- Chandler Carruth, « Efficiency with Algorithms, Performance with Data Structures » (CppCon 2014) — pourquoi la disposition mémoire domine tout ;
- Mike Acton, « Data-Oriented Design and C++ » (CppCon 2014) — la critique la plus argumentée du C++ objet naïf du point de vue de la performance ;
- les présentations de Timur Doumler et de Fedor Pikus sur les benchmarks et les micro-optimisations.
quick-bench.comet Google Benchmark : pour mesurer une fonction C++ correctement, avec gestion de l'optimisation morte.- Compiler Explorer (
godbolt.org) : voir l'assembleur généré par votre template. C'est le seul moyen de savoir si l'abstraction a été supprimée à la compilation ou si elle coûte quelque chose à l'exécution.
Priorité 3 — la portabilité de performance, pour aller plus loin¶
- Kokkos (
kokkos.org) : le modèle de programmation développé par Sandia, utilisé par de nombreux codes exascale américains. Écrire unparallel_forqui compile pour CPU multithread et pour GPU NVIDIA ou AMD. Les Kokkos Lectures, enregistrées et publiées, sont un excellent cours. - RAJA (Lawrence Livermore) : l'équivalent, avec une philosophie voisine.
- Thrust (NVIDIA, livré avec CUDA) : la STL portée sur GPU.
thrust::sort,thrust::transform,thrust::reduce. Le pont le plus court entre LAOA23 et PGPU35. - Les algorithmes parallèles du C++17 :
std::for_each(std::execution::par, …). Implémentés par GCC via Intel TBB, et par NVIDIA vianvc++ -stdparqui les exécute sur GPU. Un bon sujet d'expérimentation.
Exercices¶
E1 · ★ ⏱ 2 h — Les conteneurs et leur coût. Insérer un million d'entiers, puis
les parcourir en sommant, dans std::vector, std::list, std::deque,
std::set et std::unordered_set. Mesurer les deux phases séparément. Ordonner
les conteneurs et expliquer l'ordre par la disposition mémoire, pas par la
complexité asymptotique. std::vector doit dominer le parcours d'un facteur 10
à 50 sur std::list.
E2 · ★★ ⏱ 3 h — Foncteur, pointeur de fonction, lambda, std::function.
Écrire un transform maison générique, et lui passer la même opération sous
quatre formes : un pointeur de fonction, un foncteur (struct avec
operator()), une lambda, et un std::function. Mesurer les quatre sur dix
millions d'éléments. Puis vérifier sur Compiler Explorer lesquelles ont été
inlinées. Le std::function doit être nettement plus lent : il fait une
indirection et parfois une allocation. C'est l'exercice qui fait comprendre
pourquoi les bibliothèques HPC utilisent des templates plutôt que des
interfaces virtuelles.
E3 · ★★ ⏱ 3 h — Virtuel contre template. Un calcul sur un tableau de formes
géométriques, en deux versions : une hiérarchie avec méthode virtuelle aire(),
et une version avec std::variant ou un template. Mesurer sur un million
d'objets. Expliquer le coût de l'appel virtuel : indirection par la vtable,
impossibilité d'inliner, échec de prédiction de branchement indirect.
E4 · ★★★ ⏱ 4 h — Un conteneur pour le HPC. Écrire un aligned_vector<T>
minimal : allocation alignée sur 64 octets (une ligne de cache), interface
compatible avec les algorithmes de la STL (itérateurs, begin/end, size),
et pas d'initialisation par défaut des éléments (contrairement à
std::vector<double> qui met tout à zéro, ce qui coûte un balayage complet
inutile). Mesurer le gain sur une allocation de 1 Go suivie d'une écriture.
Bonus : implémenter un allocator personnalisé pour l'utiliser avec
std::vector directement.
E5 · ★★★ ⏱ 4 h — La STL parallèle. Comparer std::sort,
std::sort(std::execution::par, …), __gnu_parallel::sort et
thrust::sort (si vous avez un GPU) sur cent millions d'entiers. Tracer
l'accélération selon le nombre de cœurs. Sujet plus subtil qu'il n'y paraît :
la politique d'exécution parallèle nécessite une bibliothèque de support (TBB
pour libstdc++) et son absence fait silencieusement retomber en séquentiel.
E6 · ★★ ⏱ 4 h — span, vues et zéro copie. Écrire une fonction qui applique
un stencil sur une sous-partie d'un tableau, en trois versions : par indices, par
itérateurs, et par std::span. Vérifier sur Compiler Explorer que les trois
produisent le même assembleur en -O3. Puis casser volontairement l'une d'elles
— en passant par std::function, ou en ajoutant une vérification de bornes non
inlinée — et mesurer le coût. C'est l'exercice qui apprend à faire confiance à
l'abstraction après l'avoir vérifiée.
Projet¶
Projet R1 · Un mini-moteur de calcul générique
⏱ 25 h · ★★★★
Sujet. Écrire une petite bibliothèque C++ qui exprime un calcul sur
tableau une seule fois et l'exécute selon plusieurs politiques :
séquentielle, multithread (std::thread ou OpenMP), et vectorisée.
Conception attendue : une fonction template
template <typename Policy, typename Container, typename Func>
void apply(Policy p, Container& c, Func f);
avec des types de politique vides (seq_t, par_t, simd_t) qui
sélectionnent l'implémentation à la compilation par surcharge ou par
if constexpr. Aucune indirection à l'exécution : tout doit disparaître à
la compilation.
Livrables :
- La bibliothèque, en header-only, avec tests.
- Trois noyaux de démonstration : AXPY (\(y \leftarrow \alpha x + y\)), produit scalaire (avec réduction), et un stencil 1D à trois points.
- Un banc d'essai qui mesure les trois politiques sur les trois noyaux, pour des tailles couvrant L1, L3 et la mémoire principale.
- La vérification, sur Compiler Explorer ou par
objdump, que la versionsimd_tgénère bien des instructions vectorielles et que la couche d'abstraction a disparu. - Une comparaison honnête avec
std::transformet avec OpenBLAS pour l'AXPY.
Pourquoi ce projet. C'est une reconstruction miniature de Kokkos. En le faisant, vous comprendrez de l'intérieur le mécanisme qui sous-tend la portabilité de performance, et vous serez à l'aise avec Kokkos, RAJA ou SYCL quand vous les rencontrerez — en entreprise, en compétition, ou dans un code de production.
Attention au piège. L'abstraction zéro-coût n'existe que si le
compilateur inline tout. Si votre banc d'essai montre que la version
seq_t est plus lente que la boucle écrite à la main, l'abstraction fuit :
trouvez pourquoi. C'est le cœur pédagogique du projet.
Erreurs fréquentes¶
Six pièges du C++ en contexte HPC
- Croire que l'objet est gratuit. Il l'est souvent — un template inliné ne coûte rien — et parfois pas du tout : une méthode virtuelle dans une boucle chaude tue la vectorisation et coûte une indirection par itération. La règle : pas de virtuel dans le chemin chaud.
- Utiliser
std::listoustd::mapdans un code de calcul. Jamais.std::vectoret, si vraiment nécessaire,std::unordered_map. - Passer de gros objets par valeur sans s'en rendre compte, en
particulier dans les lambdas capturées par copie (
[=]). Capturez par référence ([&]) dans une boucle locale, et par valeur uniquement quand la lambda survit à son contexte — ce qui est obligatoire pour une lambda exécutée sur GPU. std::vector<double> v(n)initialise à zéro. Pour un tableau de 1 Go, c'est un balayage complet de la mémoire avant même de commencer. Selon les cas, préférezreserve+push_back, ou un allocateur qui ne construit pas par défaut.- Les exceptions dans les chemins chauds. Leur seule présence peut
empêcher certaines optimisations. Les codes HPC compilent souvent avec
-fno-exceptions. - Oublier
-O3pour mesurer un template. Sans optimisation, toutes les abstractions coûtent, et vous conclurez à tort que le C++ générique est lent. Une mesure de C++ moderne en-O0n'a aucune valeur.
Où ce module s'insère¶
| Se relie à | Comment |
|---|---|
| Le socle de première année | MEIM11 donne la mémoire ; ce module donne le langage |
| PRPA23 | MPI est une API C : les types dérivés et les tampons se manipulent mieux en sachant ce que std::vector garantit sur la contiguïté |
| PGPU35 | CUDA est du C++ ; les lambdas et les templates sont partout dans les bibliothèques modernes |
| COAV35 | Comprendre ce que le compilateur peut inliner suppose de savoir ce qu'on lui donne |
| R2 · Programmation scientifique | Le module suivant : ce langage, mis en chaîne de production |
À retenir¶
R1 en trois phrases
Quatre des cinq UE techniques du cursus exigent C ou C++, et aucune ne
l'enseigne. Ce qui compte n'est pas le C++ « objet » mais le C++
générique et zéro-coût : conteneurs contigus, algorithmes, span,
sémantique de déplacement, et la capacité à lire l'assembleur généré pour
vérifier que l'abstraction n'a rien coûté. Le piège à éviter est
d'apprendre le C++ des années 2000 : new/delete manuels, hiérarchies
profondes, fonctions virtuelles dans les boucles chaudes.
Module suivant : R2 · Programmation scientifique.