Os compiladores utilizam multithreading para tempos de compilação mais rápidos?


16

Se eu me lembro corretamente do curso dos compiladores, o compilador típico tem o seguinte esquema simplificado:

  • Um analisador lexical verifica (ou ativa alguma função de verificação) o código - fonte caractere por caractere
  • A cadeia de caracteres de entrada é comparada com o dicionário de lexemas quanto à validade
  • Se o léxico for válido, ele será classificado como o token ao qual corresponde
  • O analisador valida a sintaxe da combinação de tokens; token por token .

É teoricamente viável dividir o código-fonte em quartos (ou qualquer que seja o denominador) e multithread o processo de digitalização e análise? Existem compiladores que utilizam multithreading?




1
@RobertHarvey A principal resposta do primeiro link escreveu: "mas os próprios compiladores ainda são de thread único". Então isso é um não?
8protons

Eu sugiro que você leia o restante das respostas, especialmente esta , e o segundo link que eu postei.
Robert Harvey

2
@RobertHarvey O segundo link que você postou, pelo que entendi, está falando de um compilador que gera uma versão multithread do seu aplicativo compilado. Não é sobre o próprio compilador. Obrigado por seus recursos compartilhados e pela disponibilidade de tempo para responder.
8protons

Respostas:


29

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:

  1. 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.
  2. A parte que está sendo paralelizada geralmente leva a grande maioria do tempo de compilação.
  3. 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.

2

A compilação é um problema "embaraçosamente paralelo".

Ninguém se importa com a hora de compilar um arquivo. As pessoas se preocupam com o tempo de compilar 1000 arquivos. E para 1000 arquivos, cada núcleo do processador pode compilar um arquivo de cada vez, mantendo todos os núcleos totalmente ocupados.

Dica: "make" usa vários núcleos se você der a opção de linha de comando correta. Sem isso, ele compilará um arquivo após o outro em um sistema com 16 núcleos. O que significa que você pode compilar 16 vezes mais rápido com uma alteração de uma linha nas suas opções de compilação.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.