Grund für die enorme Größe der kompilierten ausführbaren Datei von Go

88

Ich habe ein Hallo-Welt-Go-Programm befolgt, das eine native ausführbare Datei auf meinem Linux-Computer generiert hat. Aber ich war überrascht, wie groß das einfache Hello World Go-Programm war, es war 1,9 MB groß!

Warum ist die ausführbare Datei eines so einfachen Programms in Go so groß?

Karthic Rao
quelle
20
Enorm? Dann machst du wohl nicht viel Java!
Rick-777
17
Nun, ich bin aus C / C ++ Hintergrund!
Karthic Rao
Ich habe gerade diese Scala-native Hallo-Welt ausprobiert : scala-native.org/en/latest/user/sbt.html#minimal-sbt-project Das Kompilieren, Herunterladen vieler Inhalte hat einige Zeit in Anspruch genommen , und die Binärdatei ist 3.9 MB.
bli
Ich habe meine Antwort unten mit den Ergebnissen von 2019 aktualisiert .
VonC
1
Einfache Hello World App in C # .NET Core 3.1 mit dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=truegeneriert eine Binärdatei über ~ 26MB!
Jalal

Antworten:

85

Diese genaue Frage erscheint in den offiziellen FAQ: Warum ist mein triviales Programm eine so große Binärdatei?

Zitiert die Antwort:

Die Linker in der Werkzeugkette gc ( 5l, 6l, und 8l) tun statische Verknüpfung. Alle Go-Binärdateien enthalten daher die Go-Laufzeit sowie die Informationen zum Laufzeit-Typ, die zur Unterstützung dynamischer Typprüfungen, Reflektionen und sogar Panic-Time-Stack-Traces erforderlich sind.

Ein einfaches C "Hallo, Welt" -Programm, das unter Verwendung von gcc unter Linux statisch kompiliert und verknüpft wurde, hat eine Größe von ca. 750 kB, einschließlich einer Implementierung von printf. Ein gleichwertiges Go-Programm, das verwendet fmt.Printfwird, hat eine Größe von ca. 1,9 MB. Dazu gehören jedoch eine leistungsfähigere Laufzeitunterstützung und Typinformationen.

Die native ausführbare Datei Ihrer Hello World ist also 1,9 MB groß, da sie eine Laufzeit enthält, die Speicherbereinigung, Reflektion und viele andere Funktionen bietet (die Ihr Programm möglicherweise nicht wirklich verwendet, aber vorhanden ist). Und die Implementierung des fmtPakets, mit dem Sie den "Hello World"Text gedruckt haben (plus seine Abhängigkeiten).

Versuchen Sie nun Folgendes: Fügen Sie fmt.Println("Hello World! Again")Ihrem Programm eine weitere Zeile hinzu und kompilieren Sie sie erneut. Das Ergebnis ist nicht 2x 1,9 MB, aber immer noch nur 1,9 MB! Ja, da alle verwendeten Bibliotheken ( fmtund ihre Abhängigkeiten) und die Laufzeit bereits zur ausführbaren Datei hinzugefügt wurden (und daher nur noch wenige Bytes hinzugefügt werden, um den gerade hinzugefügten zweiten Text zu drucken).

icza
quelle
8
Das statische "Hallo Welt" -Programm von AC, das statisch mit glibc verknüpft ist, beträgt 750 KB, da glibc ausdrücklich nicht für statische Verknüpfungen ausgelegt ist und in einigen Fällen sogar keine ordnungsgemäße statische Verknüpfung möglich ist. Ein "Hallo Welt" -Programm, das statisch mit musl libc verknüpft ist, ist 14K.
Craig Barnes
Ich bin immer noch auf der Suche, aber es wäre schön zu wissen, was darin verknüpft ist, damit ein Angreifer möglicherweise keinen bösen Code einbindet.
Richard
Warum befindet sich die Go-Laufzeitbibliothek nicht in einer DLL-Datei, sodass sie von allen Go-Exe-Dateien gemeinsam genutzt werden kann? Dann könnte ein "Hallo Welt" -Programm erwartungsgemäß ein paar KB statt 2 MB groß sein. Die gesamte Laufzeitbibliothek in jedem Programm zu haben, ist ein schwerwiegender Fehler für die ansonsten wunderbare Alternative zu MSVC unter Windows.
David Spector vor
Ich erwarte besser einen Einwand gegen meinen Kommentar: Go ist "statisch verknüpft". Okay, dann keine DLLs. Eine statische Verknüpfung bedeutet jedoch nicht, dass Sie eine gesamte Bibliothek verknüpfen (binden) müssen, sondern nur die Funktionen, die tatsächlich in der Bibliothek verwendet werden!
David Spector vor
39

Betrachten Sie das folgende Programm:

package main

import "fmt"

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

Wenn ich dies auf meinem Linux AMD64-Computer (Go 1.9) baue, gehen Sie wie folgt vor:

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

Ich bekomme eine Binärdatei mit einer Größe von ca. 2 MB.

Der Grund dafür (was in anderen Antworten erklärt wurde) ist, dass wir das "fmt" -Paket verwenden, das ziemlich groß ist, aber die Binärdatei wurde auch nicht entfernt und dies bedeutet, dass die Symboltabelle noch vorhanden ist. Wenn wir stattdessen den Compiler anweisen, die Binärdatei zu entfernen, wird sie viel kleiner:

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

Wenn wir das Programm jedoch neu schreiben, um die integrierte Funktion print anstelle von fmt.Println zu verwenden, gehen Sie wie folgt vor:

package main

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

Und dann kompiliere es:

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

Am Ende haben wir eine noch kleinere Binärdatei. Dies ist so klein wie möglich, ohne auf Tricks wie UPX-Packing zurückgreifen zu müssen. Der Overhead der Go-Laufzeit beträgt also ungefähr 700 KB.

Joppe
quelle
1
UPX komprimiert Binärdateien und dekomprimiert sie sofort, wenn sie ausgeführt werden. Ich würde es nicht als Trick abtun, ohne zu erklären, was es tut, da es in einigen Szenarien nützlich sein kann. Die Binärgröße wird auf Kosten der Startzeit und der RAM-Nutzung etwas reduziert. Darüber hinaus kann die Leistung ebenfalls leicht beeinträchtigt werden. Beispielsweise könnte eine ausführbare Datei auf 30% ihrer (abgespeckten) Größe verkleinert werden und die Ausführung dauert 35 ms länger.
Simlev
8

Beachten Sie, dass das Problem mit der Binärgröße durch das Problem 6853 im Golang / Go-Projekt verfolgt wird .

Zum Beispiel a26c01a begehen (für Go 1.4) Schnitt Hallo Welt von 70kB :

weil wir diese Namen nicht in die Symboltabelle schreiben.

Wenn man bedenkt, dass Compiler, Assembler, Linker und Laufzeit für 1.5 vollständig in Go sind, können Sie weitere Optimierungen erwarten.


Update 2016 Go 1.7: Dies wurde optimiert: siehe " Kleinere Go 1.7-Binärdateien ".

Aber an diesem Tag (April 2019) ist das, was am meisten Platz einnimmt runtime.pclntab.
Siehe " Warum sind meine ausführbaren Go-Dateien so groß? Größenvisualisierung von ausführbaren Go-Dateien mit D3 " von Raphael 'kena' Poss .

Es ist nicht allzu gut dokumentiert, aber dieser Kommentar aus dem Go-Quellcode legt seinen Zweck nahe:

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

Der Zweck dieser Datenstruktur besteht darin, dem Go-Laufzeitsystem zu ermöglichen, bei einem Absturz oder bei internen Anforderungen über die runtime.GetStackAPI beschreibende Stapelspuren zu erstellen .

Es scheint also nützlich zu sein. Aber warum ist es so groß?

Die in der oben verlinkten Quelldatei versteckte URL https://golang.org/s/go12symtab leitet zu einem Dokument weiter, in dem erläutert wird, was zwischen Go 1.0 und 1.2 passiert ist. Umschreiben:

Vor 1.2 gab der Go-Linker eine komprimierte Zeilentabelle aus, und das Programm dekomprimierte sie bei der Initialisierung zur Laufzeit.

In Go 1.2 wurde die Entscheidung getroffen, die Zeilentabelle in der ausführbaren Datei in ihr endgültiges Format zu erweitern, das zur direkten Verwendung zur Laufzeit ohne zusätzlichen Dekomprimierungsschritt geeignet ist.

Mit anderen Worten, das Go-Team hat beschlossen, ausführbare Dateien zu vergrößern, um Initialisierungszeit zu sparen.

Betrachtet man die Datenstruktur, so scheint es, dass die Gesamtgröße in kompilierten Binärdateien in Bezug auf die Anzahl der Funktionen im Programm superlinear ist, zusätzlich zu der Größe jeder Funktion.

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

VonC
quelle
2
Ich verstehe nicht, was die Implementierungssprache damit zu tun hat. Sie müssen gemeinsam genutzte Bibliotheken verwenden. Etwas unglaublich, dass sie es heutzutage noch nicht tun.
Marquis von Lorne
2
@EJP: Warum müssen sie gemeinsam genutzte Bibliotheken verwenden?
Flimzy
9
@EJP, ein Teil der Einfachheit von Go besteht darin, keine gemeinsam genutzten Bibliotheken zu verwenden. Tatsächlich hat Go überhaupt keine Abhängigkeiten, es verwendet einfache Systemaufrufe. Stellen Sie einfach eine einzelne Binärdatei bereit und es funktioniert einfach. Es würde die Sprache und ihr Ökosystem erheblich verletzen, wenn es anders wäre.
Creker
9
Ein oft vergessener Aspekt bei statisch verknüpften Binärdateien ist, dass sie in einem vollständig leeren Docker-Container ausgeführt werden können. Aus Sicherheitsgründen ist dies ideal. Wenn der Container leer ist, können Sie möglicherweise einbrechen (wenn die statisch verknüpfte Binärdatei Fehler aufweist). Da sich jedoch nichts im Container befindet, wird der Angriff dort gestoppt.
Joppe