Dlaczego pliki wykonywalne Rusta są tak ogromne?

153

Po znalezieniu Rusta i przeczytaniu dwóch pierwszych rozdziałów dokumentacji uważam, że podejście i sposób, w jaki zdefiniowali język, są szczególnie interesujące. Postanowiłem więc zmoczyć palce i zacząłem od Hello world ...

Zrobiłem to na Windows 7 x64, przy okazji.

fn main() {
    println!("Hello, world!");
}

Wydając cargo buildi patrząc na wynik w targets\debug, znalazłem wynikowy .exe3 MB. Po kilku poszukiwaniach (dokumentacja flag linii poleceń cargo jest trudna do znalezienia ...) znalazłem --releaseopcję i stworzyłem kompilację wydania. Ku mojemu zdziwieniu rozmiar .exe zmniejszył się tylko nieznacznie: 2,99 MB zamiast 3 MB.

Tak więc, wyznając, że jestem nowicjuszem w Rusta i jego ekosystemie, spodziewałem się, że język programowania systemów stworzy coś zwartego.

Czy ktoś może wyjaśnić, do czego Rust kompiluje się, jak to możliwe, że tworzy tak ogromne obrazy z programu 3-liniowego? Czy kompiluje się do maszyny wirtualnej? Czy istnieje polecenie strip, które przegapiłem (informacje o debugowaniu w kompilacji wydania?)? Coś jeszcze, co mogłoby pozwolić zrozumieć, co się dzieje?

BitTickler
źródło
4
Myślę, że 3Mb zawiera nie tylko Hello World, ale także całe potrzebne środowisko dla platformy. To samo można zobaczyć w przypadku Qt. Nie oznacza to, że jeśli napiszesz program 6-liniowy, rozmiar wyniesie 6 Mb. Pozostanie na poziomie 3 Mb, a potem będzie rosnąć bardzo powoli.
Andrei Nikolaenko
8
@AndreiNikolaenko Jestem tego świadomy. Ale to wskazuje, że albo nie obsługują bibliotek tak jak C, dodając tylko to, co jest wymagane do obrazu, albo że dzieje się coś innego.
BitTickler
@ user2225104 Zobacz moją odpowiedź, RUST obsługuje biblioteki w taki sam (lub podobny) sposób jak C, ale domyślnie C nie kompiluje statycznych bibliotek do twojego programu (przynajmniej w C ++).
AStopher
1
Czy to jest teraz nieaktualne? Z rustc w wersji 1.35.0 i bez opcji CLI otrzymuję plik exe o rozmiarze 137kb. Czy automatycznie kompiluje się dynamicznie połączony teraz, czy w międzyczasie wydarzyło się coś innego?
itmuckel

Odpowiedzi:

139

Rust używa statycznego linkowania do kompilowania swoich programów, co oznacza, że ​​wszystkie biblioteki wymagane przez nawet najprostszy Hello world!program zostaną wkompilowane w plik wykonywalny. Obejmuje to również środowisko wykonawcze Rust.

Aby zmusić Rusta do dynamicznego łączenia programów, użyj argumentów wiersza poleceń -C prefer-dynamic; spowoduje to znacznie mniejszy rozmiar pliku, ale będzie również wymagać, aby biblioteki Rust (w tym jego środowisko wykonawcze) były dostępne dla programu w czasie wykonywania. Zasadniczo oznacza to, że będziesz musiał je podać, jeśli komputer ich nie ma, zajmując więcej miejsca niż zajmuje oryginalny statycznie połączony program.

Ze względu na przenośność zalecałbym statyczne połączenie bibliotek Rust i środowiska uruchomieniowego w sposób, w jaki miałbyś kiedykolwiek rozprowadzać swoje programy wśród innych.

APoptor
źródło
4
@ user2225104 Nie mam pewności co do ładunku, ale zgodnie z tym raportem o błędzie na GitHub , niestety nie jest to jeszcze możliwe.
AStopher
2
Ale jak tylko będziesz mieć więcej niż 2 pliki wykonywalne rdzy w systemie, dynamiczne łączenie zacznie oszczędzać miejsce…
binki
15
Nie sądzę, aby statyczne łączenie wyjaśniało ogromny HELLO-WORLD. Czy nie powinien łączyć się tylko w tych częściach bibliotek, które są faktycznie używane, a HELLO-WORLD praktycznie nic nie używa?
MaxB
8
BitTicklercargo rustc [--debug or --release] -- -C prefer-dynamic
Zach Mertes
3
@daboross Dziękuję bardzo. Śledziłem ten powiązany RFC . Szkoda, ponieważ Rust jest również ukierunkowany na programowanie systemowe.
Franklin Yu
62

Nie mam żadnych systemów Windows do wypróbowania, ale w Linuksie statycznie skompilowany świat Rust hello jest w rzeczywistości mniejszy niż odpowiednik C.Jeśli widzisz ogromną różnicę w rozmiarze, prawdopodobnie dzieje się tak dlatego, że łączysz plik wykonywalny Rusta statycznie, a C dynamicznie.

W przypadku łączenia dynamicznego należy wziąć pod uwagę rozmiar wszystkich bibliotek dynamicznych, a nie tylko plików wykonywalnych.

Tak więc, jeśli chcesz porównać jabłka z jabłkami, upewnij się, że oba są dynamiczne lub oba są statyczne. Różne kompilatory będą miały różne wartości domyślne, więc nie możesz po prostu polegać na domyślnych ustawieniach kompilatora, aby uzyskać ten sam wynik.

Jeśli jesteś zainteresowany, oto moje wyniki:

-rw-r - r-- 1 aij aij 63 5 kwietnia 14:26 printf.c
-rwxr-xr-x 1 aij aij 6696 5 kwietnia 14:27 printf.dyn
-rwxr-xr-x 1 aij aij 829344 5 kwietnia 14:27 printf.static
-rw-r - r-- 1 aij aij 59 5 kwietnia 14:26 puts. c
-rwxr-xr-x 1 aij aij 6696 5 kwietnia 14:27 puts.dyn
-rwxr-xr-x 1 aij aij 829344 5 kwietnia 14:27 puts.static
-rwxr-xr-x 1 aij aij 8712 5 kwietnia 14:28 rust.dyn
-rw-r - r-- 1 aij aij 46 5 kwietnia 14:09 rust.rs
-rwxr-xr-x 1 aij aij 661496 5 kwietnia 14:28 rust.static

Zostały one skompilowane za pomocą gcc (Debian 4.9.2-10) 4.9.2 i rustc 1.0.0-nightly (d17d6e7f1 2015-04-02) (zbudowany 2015-04-03), zarówno z domyślnymi opcjami, jak i -staticdla gcc i -C prefer-dynamicdla rustc.

Miałem dwie wersje świata C hello, ponieważ pomyślałem, że użycie puts()może łączyć się w mniejszej liczbie jednostek kompilacji.

Jeśli chcesz spróbować odtworzyć go w systemie Windows, oto źródła, z których korzystałem:

printf.c:

#include <stdio.h>
int main() {
  printf("Hello, world!\n");
}

puts.c:

#include <stdio.h>
int main() {
  puts("Hello, world!");
}

rust.rs

fn main() {
    println!("Hello, world!");
}

Należy również pamiętać, że różne ilości informacji debugowania lub różne poziomy optymalizacji również miałyby znaczenie. Ale spodziewam się, że jeśli widzisz ogromną różnicę, jest to spowodowane łączeniem statycznym i dynamicznym.

aij
źródło
27
gcc jest wystarczająco sprytny, aby wykonać dokładnie to samo, co printf -> sam dokonuje podstawienia, dlatego wyniki są identyczne.
bluss
6
Począwszy od 2018 roku, jeśli chcesz uczciwego porównania, pamiętaj o "usunięciu" plików wykonywalnych, ponieważ witaj świecie. Plik wykonywalny Rusta w moim systemie zajmuje aż 5,3 MB, ale spada do mniej niż 10% tego po usunięciu wszystkich symboli debugowania i taki.
Matti Virkkunen
@MattiVirkkunen: Nadal sprawa w 2020 r .; naturalny rozmiar wydaje się mniejszy (nie zbliża się do 5,3 M), ale stosunek symboli do kodu jest nadal dość ekstremalny. Kompilacja debugowania, czysto domyślne opcje w Rust 1.34.0 na CentOS 7, pozbawiona strip -s, spada z 1,6M do 190K. Powstawanie uwalnianiu (domyślnych oraz opt-level='s', lto = trueoraz panic = 'abort'w celu zminimalizowania wielkości) spada od 623K do 158k.
ShadowRanger
Jak odróżnić jabłka statyczne od dynamicznych? Ten ostatni nie brzmi zdrowo.
LF
30

Podczas kompilacji z Cargo możesz użyć dynamicznego łączenia:

cargo rustc --release -- -C prefer-dynamic

Spowoduje to radykalne zmniejszenie rozmiaru pliku binarnego, ponieważ jest on teraz dynamicznie połączony.

Przynajmniej w Linuksie możesz również usunąć plik binarny symboli za pomocą strippolecenia:

strip target/release/<binary>

Spowoduje to zmniejszenie o około połowę rozmiaru większości plików binarnych.

Casper Skern Wilstrup
źródło
8
Tylko trochę statystyk, domyślna wersja hello world (linux x86_64). 3,5 m, z prefer-dynamic 8904 B, rozebrany 6392 B.
Zitrax
30

Przegląd wszystkich sposobów zmniejszania rozmiaru pliku binarnego Rusta można znaleźć w min-sized-rustrepozytorium.

Obecne kroki mające na celu zmniejszenie rozmiaru plików binarnych to:

  1. Użyj Rust 1.32.0 lub nowszego (który nie obejmuje jemallocdomyślnie)
  2. Dodaj następujące elementy do Cargo.toml
[profile.release]
opt-level = 'z'     # Optimize for size.
lto = true          # Enable Link Time Optimization
codegen-units = 1   # Reduce number of codegen units to increase optimizations.
panic = 'abort'     # Abort on panic
  1. Kompiluj w trybie wydania przy użyciu cargo build --release
  2. Uruchom stripwynikowy plik binarny.

Za pomocą nightlyRusta można zrobić więcej , ale zostawię tę informację, min-sized-rustponieważ zmienia się ona w czasie z powodu użycia niestabilnych funkcji.

Możesz także użyć #![no_std]do usunięcia Rdzy libstd. Zobacz min-sized-rustszczegóły.

feniks
źródło
-10

To jest funkcja, a nie błąd!

Możesz określić wersje biblioteki (w powiązanym z projektem pliku Cargo.toml ) używanej w programie (nawet te niejawne), aby zapewnić zgodność wersji biblioteki. To z drugiej strony wymaga, aby określona biblioteka była statycznie połączona z plikiem wykonywalnym, generując duże obrazy w czasie wykonywania.

Hej, to już nie 1978 - wiele osób ma więcej niż 2 MB RAM w swoich komputerach :-)

NPHighview
źródło
9
określenie wersji biblioteki [...] wymaga, aby dana biblioteka była statycznie połączona - nie, nie. Istnieje wiele kodów, w których dokładne wersje bibliotek są połączone dynamicznie.
Shepmaster