coldwa.st
Todos os guiasProgramaçãoWebDadosFerramentasBases de dadosHaskellConceitosCabal e buildsToolchainCompiladorDesempenhoEditor e HLS

Haskell · Cabal · desempenho de build

Paralelizar o cabal-install: como as builds Haskell aproveitam os seus núcleos

Por ColdwastAtualizado em 13 de junho de 20266 min de leitura#haskell#cabal#performance
Faixas de build paralelas mostrando pacotes Haskell independentes a serem compilados em simultâneo com cabal build -j
Os pacotes independentes no plano de instalação são construídos em paralelo — a ideia central por detrás das builds -j de hoje.
Reescrita atualizada, mantida pela comunidade. O projeto original Google Summer of Code de 2011 «Parallelising cabal-install» foi conduzido pelo anterior proprietário do domínio; este guia é um texto novo e original sobre o tema e não reproduz nem reivindica a autoria desse trabalho. Explica a ideia e atualiza-a para a toolchain de 2026.

Em 2011, construir um projeto Haskell com uma grande árvore de dependências era uma tarefa sequencial penosa: o cabal install compilava um pacote, depois o seguinte, depois o seguinte — deixando inativa a maior parte de uma máquina multinúcleo. Um projeto Google Summer of Code desse ano atacou exatamente isto: ensinar o cabal-install a construir os pacotes independentes em paralelo. Essa ideia está hoje integrada em todas as builds Haskell. Eis como funciona e como tirar dela o melhor partido hoje.

A ideia-chave: uma build é um DAG

Um plano de instalação é um grafo orientado acíclico de pacotes. Se aeson e vector dependem ambos apenas de base, não dependem um do outro — por isso não há razão para os construir um a seguir ao outro. Pode compilar simultaneamente qualquer pacote cujas dependências já estejam construídas, limitado apenas pelo número de núcleos que quiser usar.

Na prática, o construtor:

  1. Ordena topologicamente o grafo de dependências.
  2. Mantém um conjunto «pronto» de pacotes cujas dependências estão todas construídas.
  3. Lança até N builds desse conjunto em simultâneo; à medida que cada uma termina, qualquer pacote recém-desbloqueado junta-se ao conjunto pronto.

Num grafo de dependências amplo e numa máquina com 8 ou 16 núcleos, isto transforma uma build a frio de vários minutos em algo bem mais curto.

Como usá-lo hoje (2026)

Linhas de código-fonte num ecrã escuro
Linhas de código-fonte num ecrã escuro

O paralelismo ao nível do pacote está ativado por predefinição e é ajustável com -j:

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

Ou configure-o de forma persistente em cabal.project (ou ~/.cabal/config):

# cabal.project
jobs: 4

Hoje é seguro de uma forma que originalmente não era, graças às builds locais ao estilo nix (ver Cabal 2.0): cada combinação pacote-versão-opções é construída num store global endereçado por conteúdo, de modo que duas builds paralelas nunca possam sobrescrever o resultado uma da outra. O isolamento torna o paralelismo agressivo isento de riscos.

Dois níveis de paralelismo

Existem na verdade dois eixos independentes, e compreender ambos é a chave para não sobrecarregar o CPU:

  • Nível de pacote (cabal build -j): construir vários pacotes independentes de uma só vez. É o descendente do trabalho de 2011.
  • Nível de módulo (o -j próprio do GHC): dentro de um único pacote, o GHC pode compilar módulos independentes em paralelo. Ativa-o através das opções do GHC, p. ex. ghc-options: -j.

A armadilha é multiplicá-los: 8 pacotes em paralelo × 8 módulos em paralelo cada = até 64 threads GHC simultâneas numa máquina de 8 núcleos, o que abranda em vez de ajudar.

A solução moderna: um semáforo de build

As versões recentes do GHC e do Cabal resolvem o problema da sobrecarga com um semáforo de jobs partilhado. Em vez de cada pacote lançar de forma independente N jobs de módulos, o Cabal entrega ao GHC um único semáforo, para que a concorrência total — em todos os pacotes e todos os módulos — permaneça limitada por um único número:

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

Com um semáforo, «8 jobs» significa no máximo 8 tarefas de compilação GHC em curso em simultâneo no total, quer provenham dos módulos de um pacote grande quer de oito pacotes pequenos — exatamente o comportamento certo numa máquina de 8 núcleos.

Afinação prática

  • Builds CI a frio: cabal build -j (todos os núcleos) ganha quase sempre — o grafo de dependências é amplo e as máquinas de CI estão de outro modo inativas.
  • Desenvolvimento interativo: considere deixar um ou dois núcleos livres (-j$(($(nproc)-2))) para que o seu editor/LSP se mantenha reativo durante uma recompilação.
  • Muitas vezes o limite é a memória, não os núcleos. O GHC pode consumir muita RAM por compilação; se uma build paralela começar a fazer swap, baixe o -j. Os núcleos são baratos, é a pressão sobre a RAM que realmente trava as grandes builds Haskell.
  • Meça uma vez: cronometre uma build limpa a -j2, -j4, -j no seu próprio hardware e conjunto de dependências — o ponto ótimo depende de ambos.

A reter

O trabalho do GSoC 2011 respondeu a uma pergunta que ainda importa: uma build é um grafo de dependências, e os nós independentes deveriam compilar-se em simultâneo. Quinze anos depois, essa ideia é o comportamento predefinido, tornado seguro pelo armazenamento endereçado por conteúdo e refinado pelos semáforos de build. Se as suas builds Haskell ainda parecem sequenciais, está quase de certeza a deixar núcleos por aproveitar — comece com cabal build -j.


Leia também: Cabal 2.0 e as builds ao estilo nix · As sandboxes Cabal e o que as substituiu · todos os guias