Projetos de software grandes geralmente são compostos de muitas unidades de compilação que podem ser compiladas de forma relativamente independente e, portanto, a compilação geralmente é paralelizada com uma granularidade muito grosseira, invocando o compilador várias vezes em paralelo. Isso acontece no nível dos processos do SO e é coordenado pelo sistema de compilação, e não pelo compilador propriamente dito. Sei que não foi o que você pediu, mas é a coisa mais próxima da paralelização na maioria dos compiladores.
Por que é que? Bem, grande parte do trabalho que os compiladores fazem não se presta à paralelização facilmente:
- Você não pode simplesmente dividir a entrada em vários pedaços e lexá-los independentemente. Por simplicidade, você deseja dividir nos limites do lexme (para que nenhum encadeamento comece no meio de um lexme), mas determinar os limites do lexme potencialmente exige muito contexto. Por exemplo, quando você pula no meio do arquivo, você deve ter certeza de que não pulou em uma string literal. Mas, para checar isso, é preciso olhar basicamente para todos os personagens que vieram antes, o que é quase tanto trabalho quanto simplesmente exagerá-lo. Além disso, lexing raramente é o gargalo em compiladores para linguagens modernas.
- A análise é ainda mais difícil de paralelizar. Todos os problemas de dividir o texto de entrada para lexing se aplicam ainda mais à divisão dos tokens para análise - por exemplo, determinar onde uma função inicia é basicamente tão difícil quanto analisar o conteúdo da função para começar. Embora também possa haver maneiras de contornar isso, elas provavelmente serão desproporcionalmente complexas pelo pouco benefício. A análise também não é o maior gargalo.
- Depois de analisar, geralmente é necessário executar a resolução de nomes, mas isso leva a uma enorme rede entrelaçada de relacionamentos. Para resolver uma chamada de método aqui, talvez seja necessário resolver primeiro as importações neste módulo, mas elas exigem a resolução dos nomes em outra unidade de compilação, etc. O mesmo para inferência de tipo, se o seu idioma tiver esse idioma.
Depois disso, fica um pouco mais fácil. A verificação e a otimização do tipo e a geração de código podem, em princípio, ser paralelas à granularidade da função. Ainda conheço poucos ou nenhum compilador fazendo isso, talvez porque fazer qualquer tarefa desse tamanho ao mesmo tempo seja bastante desafiador. Você também deve considerar que a maioria dos grandes projetos de software contém tantas unidades de compilação que a abordagem "executar vários compiladores em paralelo" é totalmente suficiente para manter todos os seus núcleos ocupados (e, em alguns casos, até um farm de servidores inteiro). Além disso, em grandes tarefas de compilação, a E / S do disco pode ter tanto gargalo quanto o trabalho real de compilação.
Tudo isso dito, eu sei de um compilador que paralela o trabalho de geração e otimização de código. O compilador Rust pode dividir o trabalho de back-end (LLVM, que na verdade inclui otimizações de código que são tradicionalmente consideradas "meio-fim") entre vários threads. Isso é chamado de "unidades de geração de código". Em contraste com as outras possibilidades de paralelização discutidas acima, isso é econômico porque:
- A linguagem possui unidades de compilação bastante grandes (em comparação com, digamos, C ou Java); portanto, pode haver menos unidades de compilação em andamento do que os núcleos.
- A parte que está sendo paralelizada geralmente leva a grande maioria do tempo de compilação.
- O trabalho de back-end é, na maioria das vezes, embaraçosamente paralelo - apenas otimize e traduza para o código da máquina cada função independentemente. É claro que existem otimizações entre procedimentos, e as unidades de codegen as impedem e, portanto, afetam o desempenho, mas não há problemas semânticos.