coldwa.st
Alle LeitfädenProgrammierungWebDatenWerkzeugeDatenbankenHaskellKonzepteCabal & BuildsToolchainCompilerPerformanceEditor & HLS

Haskell · Cabal · Build-Performance

cabal-install parallelisieren: Wie Haskell-Builds Ihre Kerne ausnutzen

Von ColdwastAktualisiert am 13. Juni 20266 Min. Lesezeit#haskell#cabal#performance
Parallele Build-Spuren, die unabhängige Haskell-Pakete zeigen, die gleichzeitig mit cabal build -j kompiliert werden
Unabhängige Pakete im Installationsplan werden parallel gebaut — die Kernidee hinter den heutigen -j-Builds.
Aktualisierte, von der Community gepflegte Neufassung. Das ursprüngliche Google-Summer-of-Code-Projekt von 2011 „Parallelising cabal-install“ wurde vom früheren Domain-Inhaber durchgeführt; dieser Leitfaden ist ein neuer, eigenständiger Text zum Thema und gibt diese Arbeit weder wieder noch beansprucht er deren Urheberschaft. Er erklärt die Idee und bringt sie für die Toolchain von 2026 auf den neuesten Stand.

2011 war das Bauen eines Haskell-Projekts mit einem großen Abhängigkeitsbaum eine sequenzielle Plackerei: cabal install kompilierte ein Paket, dann das nächste, dann das übernächste — und ließ den Großteil einer Mehrkernmaschine ungenutzt. Ein Google-Summer-of-Code-Projekt in jenem Jahr griff genau das auf: cabal-install beizubringen, unabhängige Pakete parallel zu bauen. Diese Idee steckt heute in jedem Haskell-Build. So funktioniert sie, und so holen Sie heute das Beste heraus.

Die Kernidee: Ein Build ist ein DAG

Ein Installationsplan ist ein gerichteter azyklischer Graph von Paketen. Wenn aeson und vector beide nur von base abhängen, hängen sie nicht voneinander ab — es gibt also keinen Grund, sie nacheinander zu bauen. Sie können jedes Paket, dessen Abhängigkeiten bereits gebaut sind, gleichzeitig kompilieren, begrenzt nur durch die Anzahl der Kerne, die Sie nutzen möchten.

Konkret tut der Builder Folgendes:

  1. Er sortiert den Abhängigkeitsgraphen topologisch.
  2. Er verwaltet eine „bereit“-Menge von Paketen, deren sämtliche Abhängigkeiten gebaut sind.
  3. Er startet bis zu N Builds aus dieser Menge gleichzeitig; sobald einer fertig ist, tritt jedes neu freigeschaltete Paket der bereit-Menge bei.

Bei einem breiten Abhängigkeitsgraphen und einer Maschine mit 8 oder 16 Kernen verwandelt das einen mehrminütigen Kaltstart-Build in etwas deutlich Kürzeres.

So nutzen Sie es heute (2026)

Quellcodezeilen auf einem dunklen Bildschirm
Quellcodezeilen auf einem dunklen Bildschirm

Parallelität auf Paketebene ist standardmäßig aktiviert und über -j einstellbar:

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

Oder stellen Sie es dauerhaft in cabal.project (oder ~/.cabal/config) ein:

# cabal.project
jobs: 4

Das ist heute auf eine Weise sicher, wie es das ursprünglich nicht war, dank der nix-artigen lokalen Builds (siehe Cabal 2.0): Jede Kombination aus Paket, Version und Optionen wird in einem global inhaltsadressierten Store gebaut, sodass zwei parallele Builds einander niemals überschreiben können. Die Isolation macht aggressive Parallelität risikofrei.

Zwei Ebenen der Parallelität

Es gibt tatsächlich zwei unabhängige Achsen, und beide zu verstehen ist der Schlüssel, um Ihre CPU nicht zu überlasten:

  • Paketebene (cabal build -j): mehrere unabhängige Pakete gleichzeitig bauen. Das ist der Nachfahre der Arbeit von 2011.
  • Modulebene (GHCs eigenes -j): Innerhalb eines einzelnen Pakets kann GHC unabhängige Module parallel kompilieren. Sie aktivieren das über GHC-Optionen, z. B. ghc-options: -j.

Die Falle ist, sie zu multiplizieren: 8 Pakete parallel × je 8 Module parallel = bis zu 64 gleichzeitige GHC-Threads auf einer 8-Kern-Maschine, was bremst statt zu helfen.

Die moderne Lösung: ein Build-Semaphor

Aktuelle Versionen von GHC und Cabal lösen das Überlastungsproblem mit einem gemeinsam genutzten Job-Semaphor. Statt dass jedes Paket unabhängig N Modul-Jobs startet, übergibt Cabal an GHC ein einziges Semaphor, sodass die Gesamtnebenläufigkeit — über alle Pakete und alle Module hinweg — durch eine einzige Zahl begrenzt bleibt:

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

Mit einem Semaphor bedeutet „8 Jobs“ höchstens 8 gleichzeitig laufende GHC-Kompilieraufgaben insgesamt, ob sie nun aus den Modulen eines großen Pakets oder aus acht kleinen Paketen stammen — genau das richtige Verhalten auf einer 8-Kern-Maschine.

Praktisches Tuning

  • Kalte CI-Builds: cabal build -j (alle Kerne) gewinnt fast immer — der Abhängigkeitsgraph ist breit und die CI-Maschinen sonst ungenutzt.
  • Interaktive Entwicklung: Erwägen Sie, ein bis zwei Kerne frei zu lassen (-j$(($(nproc)-2))), damit Ihr Editor/LSP während einer Neukompilierung reaktionsschnell bleibt.
  • Oft ist der Speicher die Grenze, nicht die Kerne. GHC kann pro Kompilierung viel RAM verbrauchen; wenn ein paralleler Build ins Swappen gerät, senken Sie -j. Kerne sind billig, es ist der RAM-Druck, der große Haskell-Builds tatsächlich ausbremst.
  • Einmal messen: Stoppen Sie die Zeit eines sauberen Builds bei -j2, -j4, -j auf Ihrer eigenen Hardware und Ihrem Abhängigkeitsset — das Optimum hängt von beidem ab.

Fazit

Die GSoC-Arbeit von 2011 beantwortete eine Frage, die noch immer zählt: Ein Build ist ein Abhängigkeitsgraph, und unabhängige Knoten sollten gleichzeitig kompiliert werden. Fünfzehn Jahre später ist diese Idee das Standardverhalten, sicher gemacht durch inhaltsadressierten Speicher und verfeinert durch Build-Semaphore. Wenn sich Ihre Haskell-Builds noch sequenziell anfühlen, lassen Sie mit ziemlicher Sicherheit Kerne ungenutzt — beginnen Sie mit cabal build -j.


Lesen Sie auch: Cabal 2.0 und die nix-artigen Builds · Die Cabal-Sandboxes und was sie ersetzt hat · alle Leitfäden