Aller au contenu

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.

  1. 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.
  2. S'habituer à cppreference.com plutôt qu'à des tutoriels. La page de std::vector ou de std::transform est la source de vérité.
  3. 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.com et 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 un parallel_for qui 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 via nvc++ -stdpar qui 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 :

  1. La bibliothèque, en header-only, avec tests.
  2. Trois noyaux de démonstration : AXPY (\(y \leftarrow \alpha x + y\)), produit scalaire (avec réduction), et un stencil 1D à trois points.
  3. Un banc d'essai qui mesure les trois politiques sur les trois noyaux, pour des tailles couvrant L1, L3 et la mémoire principale.
  4. La vérification, sur Compiler Explorer ou par objdump, que la version simd_t génère bien des instructions vectorielles et que la couche d'abstraction a disparu.
  5. Une comparaison honnête avec std::transform et 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

  1. 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.
  2. Utiliser std::list ou std::map dans un code de calcul. Jamais. std::vector et, si vraiment nécessaire, std::unordered_map.
  3. 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.
  4. 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érez reserve + push_back, ou un allocateur qui ne construit pas par défaut.
  5. Les exceptions dans les chemins chauds. Leur seule présence peut empêcher certaines optimisations. Les codes HPC compilent souvent avec -fno-exceptions.
  6. Oublier -O3 pour 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 -O0 n'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.