Причина величезного розміру скомпільованого виконуваного файлу Go


90

Я виконав програму hello world Go, яка створила власний виконуваний файл на моїй машині Linux. Але я був здивований, побачивши розмір простої програми Hello world Go, вона становила 1,9 Мб!

Чому виконуваний файл такої простої програми в Go такий величезний?


22
Величезний? Я думаю, ви тоді не дуже багато працюєте з Java!
Rick-777

19
Ну, я з фону C / C ++!
Karthic Rao

Я щойно спробував цей рідний для Scala світ привітання: scala-native.org/en/latest/user/sbt.html#minimal-sbt-project На компіляцію зайняв досить тривалий час, завантаживши багато речей, а двійковий файл - 3,9 Мб.
поблизу

Я оновив свою відповідь нижче результатами 2019 року.
VonC

1
Проста програма Hello World у C # .NET Core 3.1 з dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=trueгенерує двійковий файл приблизно ~ 26 МБ!
Джалал

Відповіді:


90

Саме це запитання з’являється в офіційному FAQ: Чому моя тривіальна програма така велика двійкова?

Цитуючи відповідь:

Лінкери в ланцюзі дс інструменту ( 5l, 6lі 8l) робити статичні посилання. Тому всі двійкові файли Go містять час виконання Go, а також інформацію про тип виконання, необхідну для підтримки динамічних перевірок типу, відображення та навіть слідів стека під час паніки.

Проста програма C "привіт, світові", складена та пов'язана статично за допомогою gcc в Linux, становить близько 750 кБ, включаючи реалізацію printf. Еквівалентна програма Go, що використовує, fmt.Printfстановить близько 1,9 МБ, але це включає більш потужну підтримку часу роботи та інформацію про тип.

Отже, власний виконуваний файл Hello World становить 1,9 МБ, оскільки він містить середовище виконання, яке забезпечує збір сміття, відображення та багато інших функцій (які ваша програма може не використовувати, але вона є). І реалізація fmtпакету, який ви використовували для друку "Hello World"тексту (плюс його залежності).

Тепер спробуйте наступне: додайте ще один fmt.Println("Hello World! Again")рядок до вашої програми та скомпілюйте її знову. Результат не буде 2х 1,9 МБ, але все одно лише 1,9 МБ! Так, оскільки всі використані бібліотеки ( fmtта їх залежності) та час виконання вже додані до виконуваного файлу (і тому буде додано ще кілька байтів для друку 2-го тексту, який ви щойно додали).


10
Програма змінного струму "hello world", статично пов'язана з glibc, становить 750 Кб, оскільки glibc явно не призначений для статичного зв'язування і навіть неможливо правильно встановити статичне посилання в деяких випадках. Програма "привіт світ", статично пов'язана з musl libc - це 14K.
Крейг Барнс,

Я все ще шукаю, однак, було б непогано знати, що пов’язано, щоб зловмисник просто не пов’язував злий код.
Річард

Тож чому бібліотека середовища виконання Go не є у файлі DLL, щоб вона могла бути спільною для всіх файлів Go exe? Тоді програма "привіт світ", як очікувалося, може становити кілька Кб, а не 2 Мб. Наявність усієї бібліотеки середовища виконання в кожній програмі є фатальним недоліком для чудової альтернативи MSVC у Windows.
Девід Спектор,

Краще передбачити заперечення на мій коментар: що Go "статично пов'язаний". Гаразд, тоді ніяких бібліотек DLL. Але статичне зв’язування не означає, що вам потрібно пов’язати (прив’язати) цілу бібліотеку, лише функції, які насправді використовуються в бібліотеці!
Девід Спектор,

44

Розглянемо таку програму:

package main

import "fmt"

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

Якщо я буду це на своїй машині Linux AMD64 (Go 1.9), ось так:

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

Я отримую двійковий файл розміром близько 2 Мб.

Причиною цього (що було пояснено в інших відповідях) є те, що ми використовуємо досить великий пакет "fmt", але двійковий файл також не був вилучений, і це означає, що таблиця символів все ще є. Якщо ми замість цього доручимо компілятору видалити двійковий файл, він стане набагато меншим:

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

Однак, якщо ми перепишемо програму для використання вбудованої функції print, замість fmt.Println, ось так:

package main

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

А потім скомпілюйте його:

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

У підсумку ми отримуємо ще менший двійковий файл. Це настільки мало, наскільки ми можемо отримати його, не вдаючись до таких хитрощів, як упаковка UPX, тому загальні витрати на Go-Runtime складають приблизно 700 Кб.


3
UPX стискає двійкові файли та декомпресує їх на льоту, коли вони виконуються. Я б не відкинув цю хитрість, не пояснивши, що вона робить, оскільки це може бути корисно в деяких сценаріях. Бінарний розмір дещо зменшується за рахунок часу запуску та використання оперативної пам'яті; крім того, на продуктивність також можна трохи вплинути. Як приклад, виконуваний файл може бути зменшений до 30% від його (позбавленого) розміру і триватиме 35 мс довше.
simlev

10

Зверніть увагу, що проблема з двійковим розміром відстежується за випуском 6853 у проекті golang / go .

Наприклад, зафіксуйте a26c01a (для Go 1.4) скоротити привіт світ на 70 кБ :

тому що ми не записуємо ці імена в таблицю символів.

Враховуючи, що компілятор, асемблер, компонувальник та час виконання для 1.5 будуть повністю в Go, ви можете очікувати подальшої оптимізації.


Оновлення 2016 Go 1.7: це оптимізовано: див. « Менші двійкові файли Go 1.7 ».

Але сьогодні (квітень 2019 р.) Найбільше місця займає runtime.pclntab.
Див. " Чому мої виконувані файли Go такі великі? Візуалізація розмірів виконуваних файлів Go за допомогою D3 " від Raphael 'kena' Poss .

Це не надто добре задокументовано, однак цей коментар із вихідного коду Go вказує на його мету:

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

Метою цієї структури даних є надання можливості середовищу виконання Go створювати описові сліди стека при збої або внутрішніх запитах через runtime.GetStackAPI.

Тож це здається корисним. Але чому він такий великий?

URL-адреса https://golang.org/s/go12symtab, прихована у вищезгаданому вихідному файлі, переспрямовує на документ, який пояснює, що сталося між Go 1.0 і 1.2. Перефразовуючи:

до версії 1.2 компоновник Go видавав стиснуту таблицю рядків, і програма розпаковувала її під час ініціалізації під час виконання.

в Go 1.2 було прийнято рішення попередньо розширити таблицю рядків у виконуваному файлі до остаточного формату, придатного для безпосереднього використання під час виконання, без додаткового кроку декомпресії.

Іншими словами, команда Go вирішила збільшити виконувані файли, щоб заощадити час ініціалізації.

Крім того, розглядаючи структуру даних, виявляється, що її загальний розмір у скомпільованих двійкових файлах надзвичайно лінійний за кількістю функцій у програмі, крім того, наскільки велика кожна функція.

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


2
Я не розумію, яке відношення до нього має мова реалізації. Їм потрібно користуватися спільними бібліотеками. Дещо неймовірно, що вони цього не роблять сьогодні.
Маркіз Лорнський

2
@EJP: Чому їм потрібно користуватися спільними бібліотеками?
Неміцний

9
@EJP, частина простоти Go полягає у не використання спільних бібліотек. Насправді Go не має взагалі ніяких залежностей, він використовує прості системні виклики. Просто розгорніть один двійковий файл, і він просто працює. Це могло б значно зашкодити мові та її екосистемі, якби це було інакше.
крекер

10
Часто забувається аспект статично пов'язаних двійкових файлів полягає в тому, що це дозволяє запускати їх у абсолютно порожньому контейнері Docker. З точки зору безпеки, це ідеально. Коли контейнер порожній, можливо, ви зможете проникнути (якщо статично зв’язаний двійковий файл має недоліки), але оскільки в контейнері нічого не знайдено, атака на цьому зупиняється.
Джоппе
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.