Alasan ukuran besar kompilasi yang dapat dieksekusi dari Go

90

Saya mematuhi program hello world Go yang menghasilkan executable asli di mesin linux saya. Tapi saya terkejut melihat ukuran program Hello world Go yang sederhana, yaitu 1.9MB!

Mengapa program yang bisa dieksekusi di Go begitu besar?

Karthic Rao
sumber
22
Besar? Saya kira Anda tidak banyak melakukan Java!
Rick-777
19
Yah, saya dari latar belakang C / C ++!
Karthic Rao
Saya baru saja mencoba ini scala-native hello world: scala-native.org/en/latest/user/sbt.html#minimal-sbt-project Butuh beberapa waktu untuk mengkompilasi, mengunduh banyak barang, dan biner adalah 3.9 MB.
bli
Saya telah memperbarui jawaban saya di bawah ini dengan temuan 2019.
VonC
1
Aplikasi Hello World sederhana di C # .NET Core 3.1 dengan dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=truemenghasilkan file biner sekitar ~ 26MB!
Jalal

Jawaban:

90

Pertanyaan persis ini muncul di FAQ resmi: Mengapa program sepele saya memiliki biner yang begitu besar?

Mengutip jawabannya:

The linker dalam rantai alat gc ( 5l, 6l, dan 8l) melakukan menghubungkan statis. Oleh karena itu, semua biner Go menyertakan run-time Go, bersama dengan informasi jenis run-time yang diperlukan untuk mendukung pemeriksaan tipe dinamis, refleksi, dan bahkan pelacakan tumpukan waktu panik.

Program C "hello, world" sederhana yang dikompilasi dan ditautkan secara statis menggunakan gcc di Linux berukuran sekitar 750 kB, termasuk implementasi printf. Program Go yang setara menggunakan fmt.Printfsekitar 1,9 MB, tetapi itu termasuk dukungan run-time yang lebih kuat dan informasi jenis.

Jadi executable asli Hello World Anda adalah 1,9 MB karena berisi runtime yang menyediakan pengumpulan sampah, refleksi, dan banyak fitur lainnya (yang mungkin tidak benar-benar digunakan oleh program Anda, tetapi program itu ada di sana). Dan implementasi fmtpaket yang Anda gunakan untuk mencetak "Hello World"teks (ditambah dependensinya).

Sekarang coba yang berikut ini: tambahkan fmt.Println("Hello World! Again")baris lain ke program Anda dan kompilasi lagi. Hasilnya bukan 2x 1.9MB, tapi tetap hanya 1.9 MB! Ya, karena semua pustaka yang digunakan ( fmtdan dependensinya) dan runtime sudah ditambahkan ke file yang dapat dieksekusi (sehingga hanya beberapa byte lagi yang akan ditambahkan untuk mencetak teks ke-2 yang baru saja Anda tambahkan).

icza
sumber
10
Program AC "hello world", yang secara statis terhubung dengan glibc adalah 750K karena glibc secara jelas tidak dirancang untuk tautan statis dan bahkan tidak mungkin untuk tautan statis dengan benar dalam beberapa kasus. Program "hello world" yang secara statis terhubung dengan musl libc adalah 14K.
Craig Barnes
Saya masih mencari, bagaimanapun, alangkah baiknya mengetahui apa yang ditautkan sehingga mungkin saja penyerang tidak menautkan dalam kode jahat.
Richard
Jadi mengapa pustaka runtime Go tidak berada dalam file DLL, sehingga dapat dibagikan di antara semua file Go exe? Kemudian program "hello world" bisa berukuran beberapa KB, seperti yang diharapkan, bukan 2 MB. Memiliki seluruh runtime library di setiap program adalah kesalahan fatal bagi alternatif lain yang bagus untuk MSVC di Windows.
David Spector
Saya lebih baik mengantisipasi keberatan atas komentar saya: bahwa Go "terhubung secara statis". Oke, kalau begitu tidak ada DLL. Tetapi penautan statis tidak berarti Anda perlu menautkan (mengikat) seluruh pustaka, hanya fungsi yang benar-benar digunakan di pustaka!
David Spector
44

Pertimbangkan program berikut:

package main

import "fmt"

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

Jika saya membangun ini di mesin Linux AMD64 saya (Go 1.9), seperti ini:

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

Saya mendapatkan biner berukuran sekitar 2 Mb.

Alasan untuk ini (yang telah dijelaskan di jawaban lain) adalah karena kami menggunakan paket "fmt" yang cukup besar, tetapi binernya juga belum dihapus dan ini berarti tabel simbol masih ada. Jika kita menginstruksikan kompilator untuk menghapus biner, itu akan menjadi jauh lebih kecil:

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

Namun, jika kita menulis ulang program untuk menggunakan fungsi cetak bawaan, daripada fmt.Println, seperti ini:

package main

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

Dan kemudian kompilasi:

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

Kami berakhir dengan biner yang lebih kecil. Ini sekecil yang bisa kita dapatkan tanpa menggunakan trik seperti pengemasan UPX, jadi overhead waktu proses Go kira-kira 700 Kb.

Joppe
sumber
3
UPX mengompresi binari dan mendekompresinya dengan cepat saat dijalankan. Saya tidak akan mengabaikannya sebagai trik tanpa menjelaskan apa fungsinya, karena ini dapat berguna dalam beberapa skenario. Ukuran biner sedikit berkurang dengan mengorbankan waktu startup dan penggunaan RAM; selain itu, kinerja juga bisa sedikit terpengaruh. Sebagai contoh, file yang dapat dieksekusi dapat diperkecil hingga 30% dari ukurannya (dilucuti) dan membutuhkan waktu 35 md lebih lama untuk dijalankan.
simlev
10

Perhatikan bahwa masalah ukuran biner dilacak oleh masalah 6853 di proyek golang / go .

Misalnya, lakukan a26c01a (untuk Go 1.4) potong hello world sebesar 70kB :

karena kami tidak menulis nama-nama itu ke dalam tabel simbol.

Mengingat compiler, assembler, linker, dan runtime untuk 1.5 akan sepenuhnya ada di Go, Anda dapat mengharapkan pengoptimalan lebih lanjut.


Update 2016 Go 1.7: ini telah dioptimalkan: lihat " Smaller Go 1.7 binaries ".

Tapi hari ini (April 2019), yang paling banyak terjadi adalah runtime.pclntab.
Lihat " Mengapa file Go saya yang dapat dieksekusi begitu besar? Visualisasi ukuran file yang dapat dieksekusi Go menggunakan D3 " dari Raphael 'kena' Poss .

Ini tidak terlalu terdokumentasi dengan baik namun komentar dari kode sumber Go ini menyarankan tujuannya:

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

Tujuan dari struktur data ini adalah untuk memungkinkan sistem waktu proses Go menghasilkan jejak tumpukan deskriptif saat terjadi kerusakan atau atas permintaan internal melalui runtime.GetStackAPI.

Jadi sepertinya berguna. Tapi kenapa begitu besar?

URL https://golang.org/s/go12symtab yang disembunyikan di file sumber yang ditautkan sebelumnya dialihkan ke dokumen yang menjelaskan apa yang terjadi antara Go 1.0 dan 1.2. Untuk memparafrasekan:

sebelum 1.2, linker Go memancarkan tabel baris terkompresi, dan program akan mendekompresi saat inisialisasi pada saat run-time.

di Go 1.2, keputusan dibuat untuk memperluas tabel baris di file yang dapat dieksekusi ke dalam format akhirnya yang sesuai untuk digunakan langsung pada waktu proses, tanpa langkah dekompresi tambahan.

Dengan kata lain, tim Go memutuskan untuk memperbesar file yang dapat dieksekusi untuk menghemat waktu inisialisasi.

Juga, melihat struktur datanya, tampak bahwa ukuran keseluruhannya dalam biner yang dikompilasi adalah super linier dalam jumlah fungsi dalam program, selain seberapa besar setiap fungsi.

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

VonC
sumber
2
Saya tidak melihat apa hubungannya bahasa implementasi dengannya. Mereka perlu menggunakan perpustakaan bersama. Agak luar biasa bahwa mereka belum melakukannya di zaman sekarang ini.
Marquis dari Lorne
2
@EJP: Mengapa mereka perlu menggunakan perpustakaan bersama?
Flimzy
9
@EJP, bagian dari kesederhanaan Go adalah tidak menggunakan pustaka bersama. Faktanya, Go tidak memiliki dependensi sama sekali, Go menggunakan syscall biasa. Cukup terapkan satu biner dan itu akan berfungsi. Ini akan sangat merusak bahasa dan ekosistemnya jika sebaliknya.
creker
10
Aspek yang sering dilupakan dari memiliki binari yang terhubung secara statis adalah memungkinkan untuk menjalankannya di Docker-container yang benar-benar kosong. Dari sudut pandang keamanan, ini ideal. Saat penampung kosong, Anda mungkin dapat masuk (jika biner yang ditautkan secara statis memiliki kekurangan), tetapi karena tidak ada yang dapat ditemukan di penampung, serangan berhenti di sana.
Joppe