Haskell · Cabal · desempenho de build
Paralelizar o cabal-install: como as builds Haskell aproveitam os seus núcleos
-j de hoje.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:
- Ordena topologicamente o grafo de dependências.
- Mantém um conjunto «pronto» de pacotes cujas dependências estão todas construídas.
- 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)
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
-jpró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,-jno 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.