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

90

Я выполнил программу Hello World Go, которая генерировала собственный исполняемый файл на моей Linux-машине. Но я был удивлен, увидев размер простой программы Hello world Go, он составлял 1,9 МБ!

Почему исполняемый файл такой простой программы на Go так велик?

Картик Рао
источник
22
Огромный? Думаю, тогда вы не так много занимаетесь Java!
Rick-777,
19
Что ж, я из фона C / C ++!
Картик Рао
Я только что попробовал этот scala-native hello world: scala-native.org/en/latest/user/sbt.html#minimal-sbt-project. На компиляцию, загрузку большого количества вещей ушло довольно много времени, а размер двоичного файла 3.9. МБ.
bli
Я обновил свой ответ ниже результатами 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 "hello, world", скомпилированная и скомпилированная статически с использованием gcc в Linux, занимает около 750 КБ, включая реализацию printf. Эквивалентная программа Go использует fmt.Printfоколо 1,9 МБ, но включает более мощную поддержку времени выполнения и информацию о типе.

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

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

icza
источник
10
Программа AC "hello world", статически связанная с glibc, составляет 750 КБ, потому что glibc явно не предназначен для статического связывания и в некоторых случаях даже невозможно правильно статическое связывание. Программа "hello world", статически связанная с musl libc, имеет размер 14 КБ.
Craig Barnes
Я все еще ищу, однако, было бы неплохо узнать, что связано, чтобы злоумышленник не ссылался на злой код.
Ричард
Так почему же библиотека времени выполнения Go не находится в файле DLL, чтобы ее можно было использовать во всех exe-файлах Go? Тогда программа "hello world" может занимать несколько КБ, как и ожидалось, вместо 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 Кбайт.

Joppe
источник
3
UPX сжимает двоичные файлы и распаковывает их на лету, когда они выполняются. Я бы не стал отвергать это как трюк, не объяснив, что он делает, поскольку он может быть полезен в некоторых сценариях. Размер двоичного файла несколько уменьшается за счет времени запуска и использования оперативной памяти; кроме того, это может немного повлиять на производительность. Так же, как пример, исполняемый файл может быть уменьшен до 30% от его (удаленного) размера, и его выполнение может занять на 35 мс больше времени.
simlev
10

Обратите внимание, что проблема с размером двоичного файла отслеживается проблемой 6853 в проекте golang / go .

Например, команда commit a26c01a (для Go 1.4) сократила привет, мир на 70 КБ :

потому что мы не записываем эти имена в таблицу символов.

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


Обновление 2016 Go 1.7: это было оптимизировано: см. « Меньшие двоичные файлы Go 1.7 ».

Но в этот день (апрель 2019 года) больше всего места занимает runtime.pclntab.
См. « Почему мои исполняемые файлы Go такие большие? Визуализация размера исполняемых файлов Go с использованием D3 » от Рафаэля '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

VonC
источник
2
Я не понимаю, какое отношение к этому имеет язык реализации. Им нужно использовать общие библиотеки. Довольно невероятно, что они этого не делают в наши дни.
Marquis of Lorne
2
@EJP: Зачем им нужно использовать разделяемые библиотеки?
Flimzy
9
@EJP, часть простоты Go заключается в том, что он не использует общие библиотеки. Фактически, Go вообще не имеет никаких зависимостей, он использует простые системные вызовы. Просто разверните один двоичный файл, и он будет работать. Если бы все было иначе, это значительно повредило бы языку и его экосистеме.
creker
10
Часто забываемый аспект наличия статически связанных двоичных файлов заключается в том, что он позволяет запускать их в полностью пустом Docker-контейнере. С точки зрения безопасности это идеальный вариант. Когда контейнер пуст, вы можете взломать его (если статически связанный двоичный файл имеет недостатки), но, поскольку в контейнере ничего не найдено, атака на этом останавливается.
Joppe