Przyczyna ogromnego rozmiaru skompilowanego pliku wykonywalnego Go

90

Postępowałem zgodnie z programem hello world Go, który generował natywny plik wykonywalny na moim komputerze z systemem Linux. Ale byłem zaskoczony, widząc rozmiar prostego programu Hello world Go, to było 1,9 MB!

Dlaczego plik wykonywalny tak prostego programu w Go jest tak ogromny?

Karthic Rao
źródło
22
Olbrzymi? Myślę, że wtedy nie robisz dużo Java!
Rick-777
19
Cóż, jestem z tła C / C ++!
Karthic Rao
Właśnie wypróbowałem ten natywny dla Scala świat hello: scala-native.org/en/latest/user/sbt.html#minimal-sbt-project Kompilacja , pobieranie wielu rzeczy zajęło trochę czasu, a plik binarny to 3.9 MB.
bli
Poniżej zaktualizowałem moją odpowiedź o wyniki z 2019 r.
VonC
1
Prosta aplikacja Hello World w C # .NET Core 3.1 z dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=truegeneracją pliku binarnego o ~ 26MB!
Jalal

Odpowiedzi:

90

Dokładne pytanie pojawia się w oficjalnym FAQ: Dlaczego mój trywialny program ma tak duży plik binarny?

Cytując odpowiedź:

Łączniki w łańcuchu narzędzi GC ( 5l, 6l, i 8l) zrobić łączenie statyczne. Dlatego wszystkie pliki binarne Go zawierają czas wykonywania Go wraz z informacjami o typie czasu wykonywania niezbędnymi do obsługi dynamicznego sprawdzania typu, odbicia, a nawet śledzenia stosu czasu paniki.

Prosty program w języku C "hello, world" skompilowany i połączony statycznie przy użyciu gcc w systemie Linux zajmuje około 750 kB, łącznie z implementacją printf. Odpowiednik używanego programu Go fmt.Printfzajmuje około 1,9 MB, ale obejmuje to bardziej wydajną obsługę czasu wykonywania i informacje o typie.

Tak więc natywny plik wykonywalny twojego Hello World ma 1,9 MB, ponieważ zawiera środowisko uruchomieniowe, które zapewnia zbieranie śmieci, odbicie i wiele innych funkcji (których twój program może tak naprawdę nie używać, ale jest). I implementacja fmtpakietu, którego użyłeś do wydrukowania "Hello World"tekstu (plus jego zależności).

Teraz spróbuj wykonać następujące czynności: dodaj kolejną fmt.Println("Hello World! Again")linię do swojego programu i skompiluj go ponownie. Rezultatem nie będzie 2x 1,9 MB, ale tylko 1,9 MB! Tak, ponieważ wszystkie używane biblioteki ( fmti ich zależności) i środowisko uruchomieniowe są już dodane do pliku wykonywalnego (a więc zostanie dodanych jeszcze tylko kilka bajtów, aby wydrukować drugi tekst, który właśnie dodałeś).

icza
źródło
10
Program AC "hello world", statycznie połączony z glibc ma 750 KB, ponieważ glibc wyraźnie nie jest przeznaczony do statycznego linkowania, aw niektórych przypadkach nawet niemożliwe jest poprawne statyczne łącze. Program "hello world" statycznie powiązany z musl libc ma rozmiar 14K.
Craig Barnes
Wciąż szukam, jednak byłoby miło wiedzieć, co jest powiązane, aby po prostu atakujący nie łączył się w złym kodzie.
Richard
Dlaczego więc biblioteka wykonawcza Go nie znajduje się w pliku DLL, aby można ją było udostępniać wszystkim plikom Go exe? Wtedy program „hello world” mógłby zajmować kilka KB, zgodnie z oczekiwaniami, zamiast 2 MB. Posiadanie całej biblioteki wykonawczej w każdym programie to fatalna wada dla wspaniałej alternatywy dla MSVC w systemie Windows.
David Spector
Lepiej przewidziałbym sprzeciw wobec mojego komentarza: że Go jest „połączone statycznie”. W porządku, nie ma bibliotek DLL. Jednak łączenie statyczne nie oznacza, że ​​musisz łączyć (wiązać) całą bibliotekę, tylko te funkcje, które są faktycznie używane w bibliotece!
David Spector
43

Rozważ następujący program:

package main

import "fmt"

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

Jeśli zbuduję to na mojej maszynie Linux AMD64 (Go 1.9), na przykład:

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

Otrzymuję plik binarny o rozmiarze około 2 Mb.

Powodem tego (co zostało wyjaśnione w innych odpowiedziach) jest to, że używamy pakietu „fmt”, który jest dość duży, ale plik binarny również nie został usunięty, co oznacza, że ​​tablica symboli nadal istnieje. Jeśli zamiast tego poinstruujemy kompilator, aby usunął plik binarny, stanie się on znacznie mniejszy:

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

Jeśli jednak przepiszemy program, aby używał wbudowanej funkcji print, zamiast fmt.Println, na przykład:

package main

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

A następnie skompiluj to:

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

Otrzymujemy jeszcze mniejszy plik binarny. Jest to tak małe, jak możemy to uzyskać bez uciekania się do sztuczek, takich jak pakowanie UPX, więc narzut czasu wykonywania Go wynosi około 700 Kb.

Joppe
źródło
3
UPX kompresuje pliki binarne i dekompresuje je w locie, gdy są wykonywane. Nie odrzuciłbym tego sztuczki bez wyjaśnienia, do czego służy, ponieważ może być przydatna w niektórych scenariuszach. Rozmiar binarny jest nieco zmniejszony kosztem czasu uruchamiania i użycia pamięci RAM; co więcej, wydajność może również nieznacznie ulec zmianie. Na przykład plik wykonywalny może zostać zmniejszony do 30% swojego (pozbawionego) rozmiaru i działać dłużej o 35 ms.
simlev
10

Zauważ, że problem z rozmiarem binarnym jest śledzony według numeru 6853 w projekcie golang / go .

Na przykład, commit a26c01a (dla Go 1.4) cut hello world o 70kB :

ponieważ nie zapisujemy tych nazw w tablicy symboli.

Biorąc pod uwagę, że kompilator, asembler, konsolidator i środowisko wykonawcze w wersji 1.5 będą w całości w Go, możesz spodziewać się dalszej optymalizacji.


Aktualizacja 2016 Go 1.7: to zostało zoptymalizowane: zobacz „ Mniejsze pliki binarne Go 1.7 ”.

Ale tego dnia (kwiecień 2019) najwięcej miejsca zajmuje runtime.pclntab.
Zobacz „ Dlaczego moje pliki wykonywalne Go są tak duże? Wizualizacja rozmiaru plików wykonywalnych Go przy użyciu D3autorstwa Raphael 'kena' Poss .

Nie jest to zbyt dobrze udokumentowane, ale ten komentarz z kodu źródłowego Go sugeruje jego cel:

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

Celem tej struktury danych jest umożliwienie systemowi wykonawczemu Go tworzenia opisowych śladów stosu po awarii lub na żądanie wewnętrzne za pośrednictwem runtime.GetStackinterfejsu API.

Wydaje się więc przydatne. Ale dlaczego jest tak duży?

Adres URL https://golang.org/s/go12symtab ukryty w powyższym pliku źródłowym przekierowuje do dokumentu wyjaśniającego, co się stało między wersjami 1.0 i 1.2. Parafrazować:

przed wersją 1.2, linker Go emitował skompresowaną tabelę linii, a program dekompresował ją podczas inicjalizacji w czasie wykonywania.

w Go 1.2 podjęto decyzję o wstępnym rozszerzeniu tabeli linii w pliku wykonywalnym do ostatecznego formatu odpowiedniego do bezpośredniego użycia w czasie wykonywania, bez dodatkowego kroku dekompresji.

Innymi słowy, zespół Go zdecydował się powiększyć pliki wykonywalne, aby zaoszczędzić czas inicjalizacji.

Patrząc również na strukturę danych, okazuje się, że ich całkowity rozmiar w skompilowanych plikach binarnych jest superliniowy pod względem liczby funkcji w programie, oprócz tego, jak duża jest każda funkcja.

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

VonC
źródło
2
Nie rozumiem, co ma z tym wspólnego język implementacji. Muszą korzystać z bibliotek współdzielonych. Nieco niesamowite, że już ich nie ma w dzisiejszych czasach.
Markiz Lorne
2
@EJP: Dlaczego muszą korzystać z bibliotek współdzielonych?
Flimzy
9
@EJP, część prostoty Go polega na tym, że nie używa się bibliotek współdzielonych. W rzeczywistości Go nie ma żadnych zależności, używa zwykłych wywołań systemowych. Wystarczy wdrożyć pojedynczy plik binarny i po prostu działa. Gdyby było inaczej, znacznie zaszkodziłoby to językowi i ekosystemowi.
creker
10
Często zapominanym aspektem posiadania statycznie połączonych plików binarnych jest to, że umożliwia to uruchamianie ich w całkowicie pustym kontenerze Docker. Z punktu widzenia bezpieczeństwa jest to idealne rozwiązanie. Gdy kontener jest pusty, możesz być w stanie włamać się (jeśli statycznie połączony plik binarny ma wady), ale ponieważ w kontenerze nie ma nic do znalezienia, atak zatrzymuje się.
Joppe,