Razão para o tamanho enorme do executável compilado do Go

91

Cumpri um programa hello world Go que gerou um executável nativo na minha máquina Linux. Mas fiquei surpreso ao ver o tamanho do simples programa Hello world Go, era de 1,9 MB!

Por que o executável de um programa tão simples em Go é tão grande?

Karthic Rao
fonte
22
Enorme? Eu acho que você não faz muito Java então!
Rick-777
20
Bem, sou do fundo C / C ++!
Karthic Rao
Acabei de experimentar este hello world nativo de scala: scala-native.org/en/latest/user/sbt.html#minimal-sbt-project Demorou um pouco para compilar, baixando um monte de coisas, e o binário é 3.9 MB.
bli
Atualizei minha resposta abaixo com as descobertas de 2019.
VonC
1
O aplicativo Hello World simples em C # .NET Core 3.1 dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=truegera um arquivo binário de aproximadamente 26 MB!
Jalal

Respostas:

91

Esta pergunta exata aparece no FAQ oficial: Por que meu programa trivial é um binário tão grande?

Citando a resposta:

Os ligantes na cadeia ferramenta GC ( 5l, 6l, e 8l) fazer a ligação estática. Todos os binários Go, portanto, incluem o tempo de execução Go, junto com as informações de tipo de tempo de execução necessárias para dar suporte a verificações de tipo dinâmico, reflexão e até mesmo rastreamentos de pilha em tempo de pânico.

Um programa simples em C "hello, world" compilado e vinculado estaticamente usando gcc no Linux tem cerca de 750 kB, incluindo uma implementação de printf. Um programa Go equivalente usando fmt.Printftem cerca de 1,9 MB, mas inclui suporte de tempo de execução mais poderoso e informações de tipo.

Portanto, o executável nativo do Hello World é de 1,9 MB porque contém um tempo de execução que fornece coleta de lixo, reflexão e muitos outros recursos (que seu programa pode não usar, mas está lá). E a implementação do fmtpacote que você usou para imprimir o "Hello World"texto (mais suas dependências).

Agora tente o seguinte: adicione outra fmt.Println("Hello World! Again")linha ao seu programa e compile-o novamente. O resultado não será 2x 1,9 MB, mas apenas 1,9 MB! Sim, porque todas as bibliotecas usadas ( fmte suas dependências) e o runtime já estão adicionados ao executável (e assim apenas mais alguns bytes serão adicionados para imprimir o segundo texto que você acabou de adicionar).

icza
fonte
11
O programa AC "hello world", estaticamente vinculado à glibc tem 750K porque a glibc não é expressamente projetada para vinculação estática e é até impossível para um link estático adequado em alguns casos. Um programa "hello world" estaticamente vinculado ao musl libc tem 14K.
Craig Barnes
Ainda estou procurando, no entanto, seria bom saber o que está vinculado, para que apenas talvez um invasor não esteja vinculando em código maléfico.
Richard
Então, por que a biblioteca de tempo de execução Go não está em um arquivo DLL, para que possa ser compartilhada entre todos os arquivos exe Go? Então, um programa "hello world" poderia ter alguns KB, como esperado, em vez de 2 MB. Ter a biblioteca de tempo de execução inteira em cada programa é uma falha fatal para a alternativa maravilhosa ao MSVC no Windows.
David Spector
É melhor eu antecipar uma objeção ao meu comentário: que Go está "estaticamente vinculado". Ok, nada de DLLs então. Mas a vinculação estática não significa que você precisa vincular (vincular) uma biblioteca inteira, apenas as funções que são realmente usadas na biblioteca!
David Spector
44

Considere o seguinte programa:

package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}

Se eu construir isso em minha máquina Linux AMD64 (Go 1.9), assim:

$ go build
$ ls -la helloworld
-rwxr-xr-x 1 janf group 2029206 Sep 11 16:58 helloworld

Recebo um binário com cerca de 2 Mb de tamanho.

A razão para isso (que foi explicada em outras respostas) é que estamos usando o pacote "fmt", que é bastante grande, mas o binário também não foi removido e isso significa que a tabela de símbolos ainda está lá. Se, em vez disso, instruirmos o compilador para remover o binário, ele se tornará muito menor:

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 1323616 Sep 11 17:01 helloworld

No entanto, se reescrevermos o programa para usar a função embutida print, em vez de fmt.Println, assim:

package main

func main() {
    print("Hello World!\n")
}

E então compilar:

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 714176 Sep 11 17:06 helloworld

Acabamos com um binário ainda menor. Isso é tão pequeno quanto podemos obter sem recorrer a truques como empacotamento UPX, então a sobrecarga do tempo de execução Go é de aproximadamente 700 Kb.

Joppe
fonte
4
UPX compacta binários e os descompacta dinamicamente quando são executados. Eu não descartaria isso como um truque sem explicar o que ele faz, já que pode ser útil em alguns cenários. O tamanho binário é reduzido um pouco à custa do tempo de inicialização e do uso de RAM; além disso, o desempenho também pode ser ligeiramente afetado. Apenas como exemplo, um executável poderia ser reduzido para 30% de seu tamanho (reduzido) e levar 35 ms a mais para ser executado.
simlev
10

Observe que o problema de tamanho binário é rastreado pelo problema 6853 no projeto golang / go .

Por exemplo, o commit a26c01a (para Go 1.4) reduz o hello world em 70kB :

porque não escrevemos esses nomes na tabela de símbolos.

Considerando que o compilador, o assembler, o vinculador e o tempo de execução do 1.5 estarão inteiramente em Go, você pode esperar mais otimização.


Atualização 2016 Go 1.7: isso foi otimizado: consulte " Binários menores do Go 1.7 ".

Mas hoje (abril de 2019), o que ocupa mais lugar é runtime.pclntab.
Consulte " Por que meus arquivos executáveis ​​Go são tão grandes? Visualização do tamanho dos executáveis ​​Go usando D3 " de Raphael 'kena' Poss .

Não está muito bem documentado, no entanto, este comentário do código-fonte Go sugere seu propósito:

// A LineTable is a data structure mapping program counters to line numbers.

O objetivo desta estrutura de dados é permitir que o sistema de tempo de execução Go produza rastreamentos de pilha descritivos em uma falha ou em solicitações internas por meio do runtime.GetStack API.

Portanto, parece útil. Mas por que é tão grande?

O URL https://golang.org/s/go12symtab oculto no arquivo de origem anteriormente vinculado redireciona para um documento que explica o que aconteceu entre Go 1.0 e 1.2. Parafrasear:

antes do 1.2, o vinculador Go emitia uma tabela de linha compactada e o programa a descompactava na inicialização em tempo de execução.

no Go 1.2, foi tomada a decisão de pré-expandir a tabela de linha no arquivo executável em seu formato final adequado para uso direto em tempo de execução, sem uma etapa de descompressão adicional.

Em outras palavras, a equipe de Go decidiu tornar os arquivos executáveis ​​maiores para economizar tempo de inicialização.

Além disso, olhando para a estrutura de dados, parece que seu tamanho geral em binários compilados é superlinear no número de funções no programa, além do tamanho de cada função.

https://science.raphael.poss.name/go-executable-size-visualization-with-d3/size-demo-ss.png

VonC
fonte
2
Não vejo o que a linguagem de implementação tem a ver com isso. Eles precisam usar bibliotecas compartilhadas. Incrível que ainda não o façam nos dias de hoje.
user207421
3
@EJP: Por que eles precisam usar bibliotecas compartilhadas?
Flimzy
10
@EJP, parte da simplicidade do Go está em não usar bibliotecas compartilhadas. Na verdade, Go não tem nenhuma dependência, ele usa syscalls simples. Basta implantar um único binário e ele simplesmente funcionará. Isso prejudicaria significativamente a linguagem e seu ecossistema se fosse de outra forma.
creker
11
Um aspecto frequentemente esquecido de ter binários vinculados estaticamente é que torna possível executá-los em um contêiner do Docker completamente vazio. Do ponto de vista da segurança, isso é ideal. Quando o contêiner está vazio, você pode conseguir invadir (se o binário vinculado estaticamente tiver falhas), mas como não há nada a ser encontrado no contêiner, o ataque pára aí.
Joppe,