Como o Go compila tão rapidamente?

216

Pesquisei no Google e vasculhei o site Go, mas não consigo encontrar uma explicação para os extraordinários tempos de construção do Go. Eles são produtos dos recursos de linguagem (ou falta deles), um compilador altamente otimizado ou algo mais? Não estou tentando promover o Go; Eu só estou curioso.

Evan Kroske
fonte
12
@ Suporte, estou ciente disso. Penso que implementar um compilador de forma a compilar com rapidez perceptível é tudo, menos otimização prematura. Muito provavelmente, representa o resultado de boas práticas de design e desenvolvimento de software. Além disso, não suporto ver as palavras de Knuth tiradas de contexto e aplicadas incorretamente.
Adam Crossland
55
A versão do pessimista dessa pergunta é "Por que o C ++ compila tão lentamente?" stackoverflow.com/questions/588884/…
dan04
14
Votei para reabrir esta questão, pois não é baseada em opiniões. Pode-se dar uma boa visão geral técnica (não opinativa) das opções de idioma e / ou compilador, que facilitam a velocidade de compilação.
Martin Tournoij 28/03
Para pequenos projetos, o Go parece lento para mim. Isso porque lembro que o Turbo-Pascal era muito mais rápido em um computador que provavelmente era milhares de vezes mais lento. prog21.dadgum.com/47.html?repost=true . Toda vez que digito "vá construir" e nada acontece por alguns segundos, penso em velhos compiladores Fortran e cartões perfurados. YMMV. TLDR: "lento" e "rápido" são termos relativos.
RedGrittyBrick
Definitivamente recomendo a leitura dave.cheney.net/2014/06/07/five-things-that-make-go-fast de conhecimentos mais detalhados
Karthik

Respostas:

192

Análise de dependência.

O FAQ do Go costumava conter a seguinte frase:

O Go fornece um modelo para construção de software que facilita a análise de dependência e evita grande parte da sobrecarga de arquivos e bibliotecas de estilo C

Embora a frase não esteja mais nas Perguntas frequentes, este tópico é elaborado na palestra Go no Google , que compara a abordagem de análise de dependência de C / C ++ e Go.

Essa é a principal razão da compilação rápida. E isso é por design.

Igor Krivokon
fonte
Esta frase não está mais no FAQ da Go, mas uma explicação mais detalhada do tópico "análise de dependência" comparando a abordagem C / C ++ e Pascal / Modula / Go está disponível na palestra Go at Google
rob74
76

Acho que não é que os compiladores Go sejam rápidos , é que outros compiladores são lentos .

Os compiladores C e C ++ precisam analisar enormes quantidades de cabeçalhos - por exemplo, compilar o "hello world" em C ++ requer a compilação de 18k linhas de código, o que representa quase meio megabyte de fontes!

$ cpp hello.cpp | wc
  18364   40513  433334

Os compiladores Java e C # são executados em uma VM, o que significa que, antes que eles possam compilar qualquer coisa, o sistema operacional precisa carregar toda a VM e, em seguida, eles precisam ser compilados em JIT do bytecode para o código nativo, o que leva algum tempo.

A velocidade de compilação depende de vários fatores.

Alguns idiomas foram projetados para serem compilados rapidamente. Por exemplo, Pascal foi projetado para ser compilado usando um compilador de passagem única.

Os compiladores em si também podem ser otimizados. Por exemplo, o compilador Turbo Pascal foi escrito em um assembler otimizado à mão, que, combinado com o design da linguagem, resultou em um compilador muito rápido trabalhando em hardware de classe 286. Eu acho que mesmo agora, os compiladores Pascal modernos (por exemplo, FreePascal) são mais rápidos que os compiladores Go.

el.pescado
fonte
19
O compilador C # da Microsoft não é executado em uma VM. Ainda está escrito em C ++, principalmente por razões de desempenho.
blucz
19
Turbo Pascal e mais tarde Delphi são os melhores exemplos para compiladores incrivelmente rápidos. Depois que o arquiteto de ambos migrou para a Microsoft, vimos grandes melhorias nos compiladores e nos idiomas do MS. Isso não é uma coincidência aleatória.
TheBlastOne
7
18k linhas (18364 para ser exacto) de código é 433334 bytes (~ 0,5MB)
el.pescado
9
O compilador C # foi compilado com C # desde 2011. Apenas uma atualização caso alguém leia isso mais tarde.
Kurt Koller #
3
No entanto, o compilador C # e o CLR que executa o MSIL gerado são coisas diferentes. Estou bastante certo de que o CLR não está escrito em C #.
Jocull 12/05
39

Existem várias razões pelas quais o compilador Go é muito mais rápido que a maioria dos compiladores C / C ++:

  • Razão principal : a maioria dos compiladores C / C ++ exibe designs excepcionalmente ruins (da perspectiva da velocidade de compilação). Além disso, da perspectiva da velocidade de compilação, algumas partes do ecossistema C / C ++ (como editores nos quais os programadores estão escrevendo seus códigos) não são projetadas com a velocidade de compilação em mente.

  • Razão principal : a velocidade rápida de compilação foi uma escolha consciente no compilador Go e também no idioma Go

  • O compilador Go possui um otimizador mais simples que os compiladores C / C ++

  • Ao contrário do C ++, o Go não possui modelos e funções embutidas. Isso significa que o Go não precisa executar nenhuma instanciação de modelo ou função.

  • O compilador Go gera código de montagem de baixo nível mais cedo e o otimizador trabalha no código de montagem, enquanto em um compilador C / C ++ típico a otimização passa o trabalho em uma representação interna do código-fonte original. A sobrecarga extra no compilador C / C ++ vem do fato de que a representação interna precisa ser gerada.

  • A vinculação final (5l / 6l / 8l) de um programa Go pode ser mais lenta que a vinculação de um programa C / C ++, porque o compilador Go está passando por todo o código de montagem usado e talvez também esteja executando outras ações extras que o C / C ++ ligantes não estão fazendo

  • Alguns compiladores C / C ++ (GCC) geram instruções em formato de texto (a serem passados ​​para o assembler), enquanto o compilador Go gera instruções em formato binário. Trabalho extra (mas não muito) precisa ser feito para transformar o texto em binário.

  • O compilador Go tem como alvo apenas um pequeno número de arquiteturas de CPU, enquanto o compilador GCC tem como alvo um grande número de CPUs

  • Os compiladores projetados com o objetivo de alta velocidade de compilação, como o Jikes, são rápidos. Em uma CPU de 2 GHz, o Jikes pode compilar mais de 20.000 linhas de código Java por segundo (e o modo de compilação incremental é ainda mais eficiente).

user811773
fonte
17
O compilador do Go destaca pequenas funções. Não tenho certeza de como a segmentação de um pequeno número de CPUs o torna mais lento ... Suponho que o gcc não está gerando código PPC enquanto estou compilando para o x86.
Brad Fitzpatrick
O @BradFitzpatrick odeia ressuscitar um comentário antigo, mas, visando um número menor de plataformas, os desenvolvedores do compilador podem gastar mais tempo otimizando-o para cada um.
Persistence
usando uma forma intermediária permite suportar muito mais arquiteturas já que agora você só tem que escrever um novo backend para cada nova arquitetura
phuclv
34

A eficiência da compilação era um dos principais objetivos do projeto:

Por fim, o objetivo é que seja rápido: levará no máximo alguns segundos para criar um grande executável em um único computador. Para atingir esses objetivos, é necessário abordar uma série de questões linguísticas: um sistema do tipo expressivo, porém leve; concorrência e coleta de lixo; especificação rígida de dependência; e assim por diante. Perguntas frequentes

As perguntas frequentes sobre o idioma são bastante interessantes em relação aos recursos específicos do idioma relacionados à análise:

Segundo, o idioma foi projetado para ser fácil de analisar e pode ser analisado sem uma tabela de símbolos.

Larry OBrien
fonte
6
Isso não é verdade. Você não pode analisar completamente o código-fonte Go sem uma tabela de símbolos.
12
Também não vejo por que a coleta de lixo aprimora os tempos de compilação. Simplesmente não.
TheBlastOne
3
Estas são citações do FAQ: golang.org/doc/go_faq.html Não sei dizer se eles falharam em atingir seus objetivos (tabela de símbolos) ou se sua lógica está com defeito (GC).
quer
5
@FUZxxl Vá para golang.org/ref/spec#Primary_expressions e considere as duas seqüências [Operando, Chamada] e [Conversão]. Exemplo Ir código-fonte: identificador1 (identificador2). Sem uma tabela de símbolos, é impossível decidir se este exemplo é uma chamada ou conversão. | Qualquer idioma pode ser, em certa medida, analisado sem uma tabela de símbolos. É verdade que a maioria das partes dos códigos-fonte Go pode ser analisada sem uma tabela de símbolos, mas não é verdade que é possível reconhecer todos os elementos gramaticais definidos na especificação golang.
3
@ Atom Você trabalha duro para impedir que o analisador seja o trecho de código que relata um erro. Os analisadores geralmente fazem um mau trabalho ao relatar mensagens de erro coerentes. Aqui, você cria uma árvore de análise para a expressão como se aTypefosse uma referência variável e, posteriormente, na fase de análise semântica, quando descobre que não é para imprimir um erro significativo naquele momento.
precisa
26

Embora a maioria das opções acima seja verdadeira, há um ponto muito importante que não foi realmente mencionado: gerenciamento de dependências.

O Go precisa incluir apenas os pacotes que você está importando diretamente (como aqueles que já importaram o que precisam). Isso contrasta fortemente com o C / C ++, onde cada arquivo começa incluindo x cabeçalhos, que incluem os cabeçalhos y etc. Conclusão: A compilação do Go leva tempo linear em relação ao número de pacotes importados, onde o C / C ++ leva tempo exponencial.

Kosta
fonte
22

Um bom teste para a eficiência de conversão de um compilador é a autocompilação: quanto tempo leva um determinado compilador para se compilar? Para C ++, leva muito tempo (horas?). Em comparação, um compilador Pascal / Modula-2 / Oberon se compilaria em menos de um segundo em uma máquina moderna [1].

O Go foi inspirado por esses idiomas, mas algumas das principais razões para essa eficiência incluem:

  1. Uma sintaxe claramente definida que é matematicamente sólida, para varredura e análise eficientes.

  2. Uma linguagem com segurança de tipo e estaticamente compilada que usa compilação separada com verificação de dependência e tipo através dos limites do módulo, para evitar a leitura desnecessária de arquivos de cabeçalho e a compilação de outros módulos - em oposição à compilação independente , como em C / C ++, em que nenhuma verificação entre módulos é executada pelo compilador (daí a necessidade de reler todos esses arquivos de cabeçalho repetidas vezes, mesmo para um programa simples de uma linha "olá mundo").

  3. Uma implementação eficiente do compilador (por exemplo, análise de cima para baixo de passagem única e descida recursiva) - que obviamente é bastante auxiliada pelos pontos 1 e 2 acima.

Esses princípios já foram conhecidos e totalmente implementados nas décadas de 1970 e 1980 em idiomas como Mesa, Ada, Modula-2 / Oberon e vários outros, e só agora (na década de 2010) estão chegando a idiomas modernos como Go (Google) , Swift (Apple), C # (Microsoft) e vários outros.

Vamos torcer para que em breve essa seja a norma e não a exceção. Para chegar lá, duas coisas precisam acontecer:

  1. Primeiro, os fornecedores de plataformas de software como Google, Microsoft e Apple devem começar incentivando os desenvolvedores de aplicativos a usar a nova metodologia de compilação, permitindo que eles reutilizem sua base de códigos existente. É isso que a Apple agora está tentando fazer com a linguagem de programação Swift, que pode coexistir com o Objective-C (já que ele usa o mesmo ambiente de tempo de execução).

  2. Segundo, as próprias plataformas de software subjacentes devem ser reescritas ao longo do tempo usando esses princípios, ao mesmo tempo em que redesenham a hierarquia do módulo no processo para torná-las menos monolíticas. Obviamente, essa é uma tarefa gigantesca e pode levar a maior parte de uma década (se tiverem coragem suficiente para fazê-la - o que não tenho certeza no caso do Google).

De qualquer forma, é a plataforma que impulsiona a adoção do idioma, e não o contrário.

Referências:

[1] http://www.inf.ethz.ch/personal/wirth/ProjectOberon/PO.System.pdf , página 6: "O compilador se compila em aproximadamente 3 segundos". Esta citação é para uma placa de desenvolvimento Xilinx Spartan-3 FPGA de baixo custo, funcionando com uma frequência de clock de 25 MHz e apresentando 1 MByte de memória principal. Com isso, pode- se extrapolar facilmente para "menos de 1 segundo" para um processador moderno executando em uma freqüência de clock bem acima de 1 GHz e vários GBytes de memória principal (ou seja, várias ordens de magnitude mais poderosas que a placa FPil Xilinx Spartan-3), mesmo ao levar em consideração as velocidades de E / S. Já em 1990, quando o Oberon era executado em um processador NS32X32 de 25MHz com 2-4 MBytes de memória principal, o compilador se compilou em apenas alguns segundos. A noção de realmente esperarpara o compilador terminar um ciclo de compilação era completamente desconhecido para os programadores de Oberon naquela época. Para programas típicos, sempre levava mais tempo para remover o dedo do botão do mouse que acionava o comando de compilação do que aguardar o compilador concluir a compilação que acabou de ser acionada. Foi uma gratificação verdadeiramente instantânea, com tempos de espera próximos de zero. E a qualidade do código produzido, apesar de nem sempre estar totalmente a par dos melhores compiladores disponíveis na época, era notavelmente boa para a maioria das tarefas e bastante aceitável em geral.

Andreas
fonte
1
Um compilador Pascal / Modula-2 / Oberon / Oberon-2 seria compilado em menos de um segundo em uma máquina moderna [citation needed]
CoffeeandCode
1
Citação adicionada, ver referência [1].
555 Andreas
1
"... princípios ... encontrando seu caminho em linguagens modernas como Go (Google), Swift (Apple)" Não sei como o Swift entrou nessa lista: o compilador Swift é glacial . Em um recente encontro da CocoaHeads em Berlim, alguém forneceu alguns números para uma estrutura de tamanho médio; eles chegaram a 16 LOC por segundo.
precisa
13

O Go foi projetado para ser rápido, e isso mostra.

  1. Gerenciamento de Dependências: sem arquivo de cabeçalho, você só precisa olhar para os pacotes importados diretamente (não precisa se preocupar com o que eles importam), assim você tem dependências lineares.
  2. Gramática: a gramática da língua é simples, sendo facilmente analisada. Embora o número de recursos seja reduzido, o código do compilador em si é restrito (poucos caminhos).
  3. Nenhuma sobrecarga permitida: você vê um símbolo, sabe a qual método ele se refere.
  4. É trivialmente possível compilar o Go em paralelo, porque cada pacote pode ser compilado independentemente.

Observe que o GO não é o único idioma com esses recursos (os módulos são a norma nos idiomas modernos), mas eles o fizeram bem.

Matthieu M.
fonte
O ponto (4) não é inteiramente verdadeiro. Os módulos que dependem um do outro devem ser compilados em ordem de dependência para permitir inlining e outras coisas entre módulos.
fuz 11/03/2013
1
@FUZxxl: Porém, isso diz respeito apenas ao estágio de otimização; você pode ter um paralelismo perfeito até a geração de infra-estrutura de infra-estrutura; portanto, apenas a otimização entre módulos, o que pode ser feito no estágio do link, e o link não é paralelo de qualquer maneira. Obviamente, se você não deseja duplicar seu trabalho (re-análise), é melhor compilar de maneira "treliça": 1 / módulos sem dependência, 2 / módulos dependendo apenas de (1), 3 / módulos dependendo apenas (1) e (2), ...
Matthieu M.
2
O que é perfeitamente fácil de usar usando utilitários básicos, como um Makefile.
fuz 11/03/2013
12

Citando o livro " The Go Programming Language ", de Alan Donovan e Brian Kernighan:

A compilação Go é notavelmente mais rápida do que a maioria das outras linguagens compiladas, mesmo quando compilada do zero. Existem três razões principais para a velocidade do compilador. Primeiro, todas as importações devem ser listadas explicitamente no início de cada arquivo de origem, para que o compilador não precise ler e processar um arquivo inteiro para determinar suas dependências. Segundo, as dependências de um pacote formam um gráfico acíclico direcionado e, como não existem ciclos, os pacotes podem ser compilados separadamente e talvez em paralelo. Finalmente, o arquivo de objeto para um pacote Go compilado registra informações de exportação não apenas para o próprio pacote, mas também para suas dependências. Ao compilar um pacote, o compilador deve ler um arquivo de objeto para cada importação, mas não precisa procurar além desses arquivos.

Miscreant
fonte
9

A idéia básica da compilação é realmente muito simples. Um analisador de descida recursiva, em princípio, pode executar na velocidade de ligação de E / S. A geração de código é basicamente um processo muito simples. Uma tabela de símbolos e um sistema de tipos básicos não é algo que requer muita computação.

No entanto, não é difícil diminuir a velocidade de um compilador.

Se houver uma fase de pré-processador, com diretivas de inclusão em vários níveis , definições de macro e compilação condicional, por mais úteis que sejam essas coisas, não é difícil carregá-lo. (Por exemplo, estou pensando nos arquivos de cabeçalho do Windows e do MFC.) É por isso que os cabeçalhos pré-compilados são necessários.

Em termos de otimização do código gerado, não há limite para quanto processamento pode ser adicionado a essa fase.

Mike Dunlavey
fonte
7

Simplesmente (em minhas próprias palavras), porque a sintaxe é muito fácil (analisar e analisar)

Por exemplo, nenhuma herança de tipo significa, nenhuma análise problemática para descobrir se o novo tipo segue as regras impostas pelo tipo de base.

Por exemplo, neste exemplo de código: "interfaces", o compilador não vai e verifica se o tipo pretendido implementa a interface fornecida ao analisar esse tipo. Somente até que seja usado (e SE for usado) a verificação será realizada.

Outro exemplo, o compilador informa se você está declarando uma variável e não a está usando (ou se você deve manter um valor de retorno e não está)

O seguinte não é compilado:

package main
func main() {
    var a int 
    a = 0
}
notused.go:3: a declared and not used

Esse tipo de aplicação e princípios tornam o código resultante mais seguro, e o compilador não precisa executar validações extras que o programador pode fazer.

No geral, todos esses detalhes facilitam a análise de um idioma, o que resulta em compilações rápidas.

Mais uma vez, em minhas próprias palavras.

OscarRyz
fonte
3

Eu acho que o Go foi projetado em paralelo com a criação do compilador, então eles eram melhores amigos desde o nascimento. (IMO)

Andrey
fonte
0
  • Ir importa dependências uma vez para todos os arquivos, para que o tempo de importação não aumente exponencialmente com o tamanho do projeto.
  • Linguística mais simples significa que interpretá-los requer menos computação.

O quê mais?

Alberto Salvia Novella
fonte