coldwa.st
Tutte le guideProgrammazioneWebDatiStrumentiDatabaseHaskellConcettiCabal e buildToolchainCompilatorePrestazioniEditor e HLS

Haskell · Cabal · prestazioni di build

Parallelizzare cabal-install: come le build Haskell sfruttano i tuoi core

Di ColdwastAggiornato il 13 giugno 20266 min di lettura#haskell#cabal#performance
Corsie di build parallele che mostrano pacchetti Haskell indipendenti compilati simultaneamente con cabal build -j
I pacchetti indipendenti nel piano di installazione vengono costruiti in parallelo — l'idea centrale dietro le build -j di oggi.
Riscrittura aggiornata, mantenuta dalla community. Il progetto originale Google Summer of Code del 2011 «Parallelising cabal-install» fu condotto dal precedente proprietario del dominio; questa guida è uno scritto nuovo e originale sull'argomento e non riproduce né rivendica la paternità di quel lavoro. Spiega l'idea e la aggiorna alla toolchain del 2026.

Nel 2011, costruire un progetto Haskell con un grande albero di dipendenze era una fatica sequenziale: cabal install compilava un pacchetto, poi il successivo, poi quello dopo — lasciando inattiva la maggior parte di una macchina multicore. Un progetto Google Summer of Code di quell'anno affrontò esattamente questo: insegnare a cabal-install a costruire i pacchetti indipendenti in parallelo. Quell'idea è ormai integrata in ogni build Haskell. Ecco come funziona e come trarne il meglio oggi.

L'idea chiave: una build è un DAG

Un piano di installazione è un grafo orientato aciclico di pacchetti. Se aeson e vector dipendono entrambi solo da base, non dipendono l'uno dall'altro — quindi non c'è alcun motivo di costruirli uno dopo l'altro. Puoi compilare simultaneamente qualunque pacchetto le cui dipendenze siano già costruite, limitato solo dal numero di core che vuoi usare.

Concretamente, il costruttore:

  1. Ordina topologicamente il grafo delle dipendenze.
  2. Mantiene un insieme «pronto» di pacchetti le cui dipendenze sono tutte costruite.
  3. Avvia fino a N build da quell'insieme simultaneamente; man mano che ciascuna termina, ogni pacchetto appena sbloccato entra nell'insieme pronto.

Su un grafo di dipendenze ampio e una macchina con 8 o 16 core, questo trasforma una build a freddo di diversi minuti in qualcosa di molto più breve.

Come usarlo oggi (2026)

Righe di codice sorgente su uno schermo scuro
Righe di codice sorgente su uno schermo scuro

Il parallelismo a livello di pacchetto è attivo per impostazione predefinita e regolabile con -j:

$ cabal build -j         # use all available cores
$ cabal build -j4        # cap at 4 concurrent package builds

Oppure impostalo in modo persistente in cabal.project (o ~/.cabal/config):

# cabal.project
jobs: 4

Oggi è sicuro in un modo in cui in origine non lo era, grazie alle build locali in stile nix (vedi Cabal 2.0): ogni combinazione pacchetto-versione-opzioni viene costruita in uno store globale indirizzato per contenuto, così che due build parallele non possano mai sovrascrivere l'output l'una dell'altra. L'isolamento rende il parallelismo aggressivo senza rischi.

Due livelli di parallelismo

Ci sono in realtà due assi indipendenti, e comprenderli entrambi è la chiave per non sovraccaricare la CPU:

  • Livello pacchetto (cabal build -j): costruire più pacchetti indipendenti alla volta. È il discendente del lavoro del 2011.
  • Livello modulo (il -j proprio di GHC): all'interno di un singolo pacchetto, GHC può compilare moduli indipendenti in parallelo. Lo attivi tramite le opzioni GHC, ad es. ghc-options: -j.

La trappola è moltiplicarli: 8 pacchetti in parallelo × 8 moduli in parallelo ciascuno = fino a 64 thread GHC simultanei su una macchina a 8 core, il che rallenta invece di aiutare.

La soluzione moderna: un semaforo di build

Le versioni recenti di GHC e Cabal risolvono il problema del sovraccarico con un semaforo di job condiviso. Invece che ogni pacchetto avvii indipendentemente N job di moduli, Cabal consegna a GHC un singolo semaforo, così che la concorrenza totale — su tutti i pacchetti e tutti i moduli — resti limitata da un unico numero:

# cabal.project
jobs: 8
package *
  ghc-options: -jsem    # coordinate module + package parallelism
                        # via a shared semaphore

Con un semaforo, «8 job» significa al massimo 8 attività di compilazione GHC in corso simultaneamente in totale, che provengano dai moduli di un grosso pacchetto o da otto piccoli pacchetti — esattamente il comportamento giusto su una macchina a 8 core.

Regolazione pratica

  • Build CI a freddo: cabal build -j (tutti i core) vince quasi sempre — il grafo delle dipendenze è ampio e le macchine CI sono altrimenti inattive.
  • Sviluppo interattivo: valuta di lasciare uno o due core liberi (-j$(($(nproc)-2))) affinché il tuo editor/LSP resti reattivo durante una ricompilazione.
  • Spesso il limite è la memoria, non i core. GHC può consumare molta RAM per compilazione; se una build parallela inizia a fare swap, abbassa -j. I core costano poco, è la pressione sulla RAM a bloccare davvero le grandi build Haskell.
  • Misura una volta: cronometra una build pulita a -j2, -j4, -j sul tuo hardware e sul tuo set di dipendenze — il punto ottimale dipende da entrambi.

Da ricordare

Il lavoro del GSoC 2011 rispose a una domanda che conta ancora: una build è un grafo di dipendenze, e i nodi indipendenti dovrebbero compilarsi simultaneamente. Quindici anni dopo, quell'idea è il comportamento predefinito, reso sicuro dallo storage indirizzato per contenuto e affinato dai semafori di build. Se le tue build Haskell sembrano ancora sequenziali, quasi certamente stai lasciando core inutilizzati — inizia con cabal build -j.


Leggi anche: Cabal 2.0 e le build in stile nix · Le sandbox Cabal e ciò che le ha sostituite · tutte le guide