coldwa.st
Tous les guidesProgrammationWebDonnéesOutilsBases de donnéesHaskellConceptsCabal & buildsChaîne d’outilsCompilateurPerformanceÉditeur & HLS

Haskell · Cabal · performance de build

Paralléliser cabal-install : comment les builds Haskell exploitent vos cœurs

Par ColdwastMis à jour le 13 juin 20266 min de lecture#haskell#cabal#performance
Des voies de build parallèles montrant des paquets Haskell indépendants compilés simultanément avec cabal build -j
Les paquets indépendants du plan d’installation se construisent en parallèle — l’idée centrale derrière les builds -j d’aujourd’hui.
Réécriture mise à jour, maintenue par la communauté. Le projet Google Summer of Code original de 2011 « Parallelising cabal-install » a été mené par l’ancien propriétaire du domaine ; ce guide est un écrit neuf et original sur le sujet et ne reproduit ni ne revendique la paternité de ce travail. Il explique l’idée et l’actualise pour la chaîne d’outils 2026.

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 :

  1. Trie topologiquement le graphe de dépendances.
  2. Maintient un ensemble « prêt » de paquets dont toutes les dépendances sont construites.
  3. 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)

Lignes de code source sur un écran sombre
Lignes de code source sur un écran sombre

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 -j propre à 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, -j sur 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.


À lire aussi : Cabal 2.0 et les builds façon nix · Les sandboxes Cabal et ce qui les a remplacées · tous les guides