Raison de la taille énorme de l'exécutable compilé de Go

90

J'ai respecté un programme hello world Go qui a généré un exécutable natif sur ma machine Linux. Mais j'ai été surpris de voir la taille du simple programme Hello world Go, il était de 1,9 Mo!

Pourquoi l'exécutable d'un programme aussi simple dans Go est-il si énorme?

Karthic Rao
la source
22
Énorme? Je suppose que vous ne faites pas beaucoup de Java alors!
Rick-777
19
Eh bien, je suis du fond C / C ++!
Karthic Rao
Je viens d'essayer ce hello world natif de scala: scala-native.org/en/latest/user/sbt.html#minimal-sbt-project Il a fallu un certain temps pour compiler, télécharger beaucoup de choses, et le binaire est 3,9 MB.
bli
J'ai mis à jour ma réponse ci-dessous avec les résultats de 2019.
VonC le
1
L'application Hello World simple en C # .NET Core 3.1 avec dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=truegénère un fichier binaire d'environ 26 Mo!
Jalal

Réponses:

90

Cette question exacte apparaît dans la FAQ officielle: Pourquoi mon programme trivial est-il un si gros binaire?

Citant la réponse:

Les lieurs dans la chaîne d'outils de gc ( 5l, 6l, et 8l) font la liaison statique. Tous les fichiers binaires Go incluent donc l'environnement d'exécution Go, ainsi que les informations de type d'exécution nécessaires pour prendre en charge les vérifications de type dynamiques, la réflexion et même les traces de pile de panique.

Un simple programme C "hello, world" compilé et lié statiquement à l'aide de gcc sous Linux fait environ 750 Ko, y compris une implémentation de printf. Un programme Go équivalent utilise fmt.Printfenviron 1,9 Mo, mais cela inclut un support d'exécution plus puissant et des informations de type.

Ainsi, l'exécutable natif de votre Hello World fait 1,9 Mo car il contient un runtime qui fournit le ramasse-miettes, la réflexion et de nombreuses autres fonctionnalités (que votre programme n'utilise peut-être pas vraiment, mais il est là). Et l'implémentation du fmtpackage que vous avez utilisé pour imprimer le "Hello World"texte (plus ses dépendances).

Maintenant, essayez ce qui suit: ajoutez une autre fmt.Println("Hello World! Again")ligne à votre programme et compilez-la à nouveau. Le résultat ne sera pas 2x 1,9 Mo, mais toujours seulement 1,9 Mo! Oui, car toutes les bibliothèques utilisées ( fmtet ses dépendances) et le runtime sont déjà ajoutés à l'exécutable (et donc juste quelques octets supplémentaires seront ajoutés pour imprimer le 2ème texte que vous venez d'ajouter).

icza
la source
10
Le programme AC "hello world", lié statiquement à la glibc, fait 750K car la glibc n'est expressément pas conçue pour la liaison statique et il est même impossible de créer une liaison correctement statique dans certains cas. Un programme "hello world" lié statiquement avec musl libc fait 14K.
Craig Barnes
Je cherche toujours, cependant, ce serait bien de savoir ce qui est lié afin que peut-être un attaquant ne crée pas de lien dans un code maléfique.
Richard
Alors, pourquoi la bibliothèque d'exécution Go n'est-elle pas dans un fichier DLL, de sorte qu'elle puisse être partagée entre tous les fichiers Go exe? Alors un programme "bonjour le monde" pourrait faire quelques Ko, comme prévu, au lieu de 2 Mo. Avoir toute la bibliothèque d'exécution dans chaque programme est une faille fatale pour l'alternative par ailleurs merveilleuse à MSVC sous Windows.
David Spector
Je ferais mieux d'anticiper une objection à mon commentaire: que Go est "statiquement lié". D'accord, pas de DLL alors. Mais la liaison statique ne signifie pas que vous devez lier (lier) une bibliothèque entière, seulement les fonctions qui sont réellement utilisées dans la bibliothèque!
David Spector
44

Considérez le programme suivant:

package main

import "fmt"

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

Si je construis ceci sur ma machine Linux AMD64 (Go 1.9), comme ceci:

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

J'obtiens un binaire d'environ 2 Mo de taille.

La raison à cela (qui a été expliquée dans d'autres réponses) est que nous utilisons le package "fmt" qui est assez volumineux, mais le binaire n'a pas non plus été supprimé et cela signifie que la table des symboles est toujours là. Si nous demandons au compilateur de supprimer le binaire, il deviendra beaucoup plus petit:

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

Cependant, si nous réécrivons le programme pour utiliser la fonction intégrée print, au lieu de fmt.Println, comme ceci:

package main

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

Et puis compilez-le:

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

Nous nous retrouvons avec un binaire encore plus petit. C'est aussi petit que nous pouvons l'obtenir sans recourir à des astuces comme UPX-packing, donc la surcharge du Go-runtime est d'environ 700 Ko.

Joppe
la source
3
UPX compresse les binaires et les décompresse à la volée lorsqu'ils sont exécutés. Je ne le rejetterais pas sans expliquer ce qu'il fait, car cela peut être utile dans certains scénarios. La taille binaire est quelque peu réduite au détriment du temps de démarrage et de l'utilisation de la RAM; de plus, les performances peuvent également être légèrement affectées. À titre d'exemple, un exécutable pourrait être réduit à 30% de sa taille (dépouillée) et prendre 35 ms de plus à s'exécuter.
simlev du
10

Notez que le problème de taille binaire est suivi par le problème 6853 dans le projet golang / go .

Par exemple, commit a26c01a (pour Go 1.4) réduit hello world de 70 ko :

car nous n'écrivons pas ces noms dans la table des symboles.

Étant donné que le compilateur, l'assembleur, l'éditeur de liens et le runtime pour 1.5 seront entièrement dans Go, vous pouvez vous attendre à une optimisation supplémentaire.


Mise à jour 2016 Go 1.7: cela a été optimisé: voir " Smaller Go 1.7 binaires ".

Mais ces jours-ci (avril 2019), ce qui prend le plus de place est runtime.pclntab.
Voir « Pourquoi mes fichiers exécutables Go sont-ils si volumineux? Visualisation de la taille des exécutables Go à l'aide de D3 » de Raphael 'kena' Poss .

Ce n'est pas trop bien documenté, mais ce commentaire du code source de Go suggère son objectif:

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

Le but de cette structure de données est de permettre au système d'exécution Go de produire des traces de pile descriptives en cas de panne ou de requêtes internes via l' runtime.GetStackAPI.

Cela semble donc utile. Mais pourquoi est-il si grand?

L'URL https://golang.org/s/go12symtab cachée dans le fichier source lié ci-dessus redirige vers un document qui explique ce qui s'est passé entre Go 1.0 et 1.2. Paraphraser:

avant la version 1.2, l'éditeur de liens Go émettait une table de lignes compressée et le programme la décompressait lors de l'initialisation au moment de l'exécution.

dans Go 1.2, il a été décidé de pré-étendre la table de lignes du fichier exécutable dans son format final adapté à une utilisation directe au moment de l'exécution, sans étape de décompression supplémentaire.

En d'autres termes, l'équipe Go a décidé d'agrandir les fichiers exécutables pour gagner du temps lors de l'initialisation.

En outre, en regardant la structure des données, il semble que sa taille globale dans les binaires compilés est super-linéaire dans le nombre de fonctions dans le programme, en plus de la taille de chaque fonction.

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

VonC
la source
2
Je ne vois pas ce que le langage d'implémentation a à voir avec cela. Ils doivent utiliser des bibliothèques partagées. Un peu incroyable qu'ils ne le soient pas déjà de nos jours.
Marquis de Lorne
2
@EJP: Pourquoi ont-ils besoin d'utiliser des bibliothèques partagées?
Flimzy
9
@EJP, une partie de la simplicité de Go est de ne pas utiliser de bibliothèques partagées. En fait, Go n'a pas du tout de dépendances, il utilise des appels système simples. Déployez simplement un seul binaire et cela fonctionne. Cela nuirait considérablement à la langue et à son écosystème s'il en était autrement.
creker
10
Un aspect souvent oublié d'avoir des binaires liés statiquement est qu'il permet de les exécuter dans un conteneur Docker complètement vide. Du point de vue de la sécurité, c'est idéal. Lorsque le conteneur est vide, vous pourrez peut-être entrer par effraction (si le binaire lié statiquement a des défauts), mais comme il n'y a rien à trouver dans le conteneur, l'attaque s'arrête là.
Joppe du