Haskell · Cabal · performance de build
Paralléliser cabal-install : comment les builds Haskell exploitent vos cœurs
-j d’aujourd’hui.En 2011, construire un projet Haskell avec un grand arbre de dépendances était une corvée séquentielle : cabal install compilait un paquet, puis le suivant, puis le suivant — laissant l’essentiel d’une machine multicœur inactif. Un projet Google Summer of Code cette année-là s’est attaqué exactement à cela : apprendre à cabal-install à construire les paquets indépendants en parallèle. Cette idée est désormais intégrée à chaque build Haskell. Voici comment elle fonctionne, et comment en tirer le meilleur aujourd’hui.
L’idée clé : un build est un DAG
Un plan d’installation est un graphe orienté acyclique de paquets. Si aeson et vector dépendent tous deux uniquement de base, ils ne dépendent pas l’un de l’autre — il n’y a donc aucune raison de les construire l’un après l’autre. Vous pouvez compiler simultanément tout paquet dont les dépendances sont déjà construites, borné uniquement par le nombre de cœurs que vous voulez utiliser.
Concrètement, le constructeur :
- Trie topologiquement le graphe de dépendances.
- Maintient un ensemble « prêt » de paquets dont toutes les dépendances sont construites.
- Lance jusqu’à N builds de cet ensemble simultanément ; à mesure que chacun se termine, tout paquet nouvellement débloqué rejoint l’ensemble prêt.
Sur un graphe de dépendances large et une machine à 8 ou 16 cœurs, cela transforme un build à froid de plusieurs minutes en quelque chose de bien plus court.
Comment l’utiliser aujourd’hui (2026)
Le parallélisme au niveau des paquets est activé par défaut et réglable avec -j :
$ cabal build -j # use all available cores
$ cabal build -j4 # cap at 4 concurrent package builds Ou réglez-le de manière persistante dans cabal.project (ou ~/.cabal/config) :
# cabal.project
jobs: 4 C’est sûr aujourd’hui d’une manière qui ne l’était pas à l’origine, grâce aux builds locaux façon nix (voir Cabal 2.0) : chaque combinaison paquet-version-options se construit dans un store global adressé par contenu, de sorte que deux builds parallèles ne peuvent jamais écraser la sortie l’un de l’autre. L’isolation rend le parallélisme agressif sans risque.
Deux niveaux de parallélisme
Il y a en réalité deux axes indépendants, et comprendre les deux est la clé pour éviter de surcharger votre CPU :
- Niveau paquet (
cabal build -j) : construire plusieurs paquets indépendants à la fois. C’est le descendant du travail de 2011. - Niveau module (le
-jpropre à GHC) : au sein d’un même paquet, GHC peut compiler des modules indépendants en parallèle. Vous l’activez via les options GHC, par ex.ghc-options: -j.
Le piège est de les multiplier : 8 paquets en parallèle × 8 modules en parallèle chacun = jusqu’à 64 threads GHC simultanés sur une machine à 8 cœurs, ce qui ralentit au lieu d’aider.
La solution moderne : un sémaphore de build
Les versions récentes de GHC et Cabal résolvent le problème de surcharge avec un sémaphore de jobs partagé. Au lieu que chaque paquet lance indépendamment N jobs de modules, Cabal remet à GHC un seul sémaphore afin que la concurrence totale — pour tous les paquets et tous les modules — reste bornée par un seul nombre :
# cabal.project
jobs: 8
package *
ghc-options: -jsem # coordinate module + package parallelism
# via a shared semaphore Avec un sémaphore, « 8 jobs » signifie au plus 8 tâches de compilation GHC en cours simultanément au total, qu’elles proviennent des modules d’un gros paquet ou de huit petits paquets — exactement le bon comportement sur une machine à 8 cœurs.
Réglage pratique
- Builds CI à froid :
cabal build -j(tous les cœurs) est presque toujours gagnant — le graphe de dépendances est large et les machines CI sont sinon inactives. - Développement interactif : envisagez de laisser un ou deux cœurs libres (
-j$(($(nproc)-2))) pour que votre éditeur/LSP reste réactif pendant une recompilation. - La mémoire, pas les cœurs, est souvent la limite. GHC peut consommer beaucoup de RAM par compilation ; si un build parallèle se met à swapper, baissez
-j. Les cœurs sont bon marché, c’est la pression sur la RAM qui bloque réellement les gros builds Haskell. - Mesurez une fois : chronométrez un build propre à
-j2,-j4,-jsur votre propre matériel et votre jeu de dépendances — le point optimal dépend des deux.
À retenir
Le travail du GSoC 2011 a répondu à une question qui compte encore : un build est un graphe de dépendances, et les nœuds indépendants devraient se compiler simultanément. Quinze ans plus tard, cette idée est le comportement par défaut, rendue sûre par le stockage adressé par contenu et affinée par les sémaphores de build. Si vos builds Haskell semblent encore séquentiels, vous laissez presque certainement des cœurs sur la table — commencez par cabal build -j.