Razón del enorme tamaño del ejecutable compilado de Go

90

Cumplí con un programa Hello World Go que generó un ejecutable nativo en mi máquina Linux. Pero me sorprendió ver el tamaño del sencillo programa Hello world Go, ¡era de 1,9 MB!

¿Por qué el ejecutable de un programa tan simple en Go es tan grande?

Karthic Rao
fuente
22
¿Enorme? ¡Supongo que entonces no haces mucho Java!
Rick-777
19
Bueno, soy de C / C ++.
Karthic Rao
Acabo de probar este hola mundo nativo de scala: scala-native.org/en/latest/user/sbt.html#minimal-sbt-project Me llevó bastante tiempo compilar, descargar muchas cosas y el binario es 3.9 MEGABYTE.
bli
He actualizado mi respuesta a continuación con los resultados de 2019.
VonC
1
¡La aplicación simple Hello World en C # .NET Core 3.1 dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=truegenera un archivo binario de aproximadamente ~ 26 MB!
Jalal

Respuestas:

90

Esta pregunta exacta aparece en las preguntas frecuentes oficiales: ¿Por qué mi programa trivial es un binario tan grande?

Citando la respuesta:

Los enlazadores en la cadena de herramientas gc ( 5l, 6ly 8l) hacen la vinculación estática. Por lo tanto, todos los binarios de Go incluyen el tiempo de ejecución de Go, junto con la información de tipo de tiempo de ejecución necesaria para admitir comprobaciones de tipo dinámico, reflexión e incluso seguimientos de pila en tiempo de pánico.

Un programa simple de C "hola, mundo" compilado y enlazado estáticamente usando gcc en Linux tiene alrededor de 750 kB, incluida una implementación de printf. El uso de un programa Go equivalente fmt.Printfes de alrededor de 1,9 MB, pero eso incluye información de tipo y soporte de tiempo de ejecución más potente.

Entonces, el ejecutable nativo de su Hello World es de 1.9 MB porque contiene un tiempo de ejecución que proporciona recolección de basura, reflexión y muchas otras características (que su programa podría no usar realmente, pero está ahí). Y la implementación del fmtpaquete que usaste para imprimir el "Hello World"texto (más sus dependencias).

Ahora intente lo siguiente: agregue otra fmt.Println("Hello World! Again")línea a su programa y compílelo nuevamente. El resultado no será 2x 1,9 MB, ¡sino solo 1,9 MB! Sí, porque todas las bibliotecas utilizadas ( fmty sus dependencias) y el tiempo de ejecución ya están agregadas al ejecutable (por lo que solo se agregarán unos pocos bytes más para imprimir el segundo texto que acaba de agregar).

icza
fuente
10
El programa AC "hello world", vinculado estáticamente con glibc es de 750K porque glibc no está diseñado expresamente para la vinculación estática e incluso es imposible establecer una vinculación estática adecuada en algunos casos. Un programa de "hola mundo" vinculado estáticamente con musl libc es 14K.
Craig Barnes
Todavía estoy buscando, sin embargo, sería bueno saber qué está vinculado para que tal vez un atacante no lo esté en un código maligno.
Richard
Entonces, ¿por qué la biblioteca de tiempo de ejecución de Go no está en un archivo DLL, de modo que pueda compartirse entre todos los archivos exe de Go? Entonces, un programa de "hola mundo" podría tener unos pocos KB, como se esperaba, en lugar de 2 MB. Tener toda la biblioteca de tiempo de ejecución en cada programa es un defecto fatal para la maravillosa alternativa a MSVC en Windows.
David Spector
Será mejor que anticipe una objeción a mi comentario: que Go está "vinculado estáticamente". Bueno, entonces no hay DLL. Pero la vinculación estática no significa que deba vincular (vincular) una biblioteca completa, ¡solo las funciones que realmente se usan en la biblioteca!
David Spector
43

Considere el siguiente programa:

package main

import "fmt"

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

Si construyo esto en mi máquina Linux AMD64 (Go 1.9), así:

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

Obtengo un binario de aproximadamente 2 Mb de tamaño.

La razón de esto (que se ha explicado en otras respuestas) es que estamos usando el paquete "fmt" que es bastante grande, pero el binario tampoco se ha eliminado y esto significa que la tabla de símbolos todavía está allí. Si, en cambio, le indicamos al compilador que elimine el binario, se volverá mucho más pequeño:

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

Sin embargo, si reescribimos el programa para usar la función incorporada print, en lugar de fmt.Println, así:

package main

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

Y luego compílelo:

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

Terminamos con un binario aún más pequeño. Esto es lo más pequeño que podemos obtener sin recurrir a trucos como el empaquetado UPX, por lo que la sobrecarga del tiempo de ejecución de Go es de aproximadamente 700 Kb.

Joppe
fuente
3
UPX comprime los binarios y los descomprime sobre la marcha cuando se ejecutan. No descartaría un truco sin explicar qué hace, ya que puede ser útil en algunos escenarios. El tamaño binario se reduce un poco a expensas del tiempo de inicio y el uso de RAM; además, el rendimiento también puede verse afectado ligeramente. A modo de ejemplo, un ejecutable podría reducirse al 30% de su tamaño (eliminado) y tardar 35 ms en ejecutarse.
simlev
10

Tenga en cuenta que el problema del tamaño binario se rastrea mediante el problema 6853 en el proyecto golang / go .

Por ejemplo, comete a26c01a (para Go 1.4) corte hello world en 70kB :

porque no escribimos esos nombres en la tabla de símbolos.

Teniendo en cuenta que el compilador, el ensamblador, el enlazador y el tiempo de ejecución para 1.5 estarán completamente en Go, puede esperar una mayor optimización.


Actualización 2016 Go 1.7: se ha optimizado: consulte " Binarios de Go 1.7 más pequeños ".

Pero en estos días (abril de 2019), lo que más ocupa es runtime.pclntab.
Consulte " ¿Por qué mis archivos ejecutables Go son tan grandes? Visualización del tamaño de los ejecutables Go usando D3 " de Raphael 'kena' Poss .

No está muy bien documentado, sin embargo, este comentario del código fuente de Go sugiere su propósito:

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

El propósito de esta estructura de datos es permitir que el sistema de tiempo de ejecución de Go produzca seguimientos descriptivos de la pila en caso de falla o solicitudes internas a través de la runtime.GetStackAPI.

Entonces parece útil. Pero, ¿por qué es tan grande?

La URL https://golang.org/s/go12symtab oculta en el archivo fuente anteriormente vinculado redirige a un documento que explica lo que sucedió entre Go 1.0 y 1.2. Parafrasear:

antes de la versión 1.2, el enlazador Go emitía una tabla de líneas comprimidas y el programa la descomprimía al inicializarse en tiempo de ejecución.

en Go 1.2, se tomó la decisión de expandir previamente la tabla de líneas en el archivo ejecutable en su formato final adecuado para uso directo en tiempo de ejecución, sin un paso de descompresión adicional.

En otras palabras, el equipo de Go decidió agrandar los archivos ejecutables para ahorrar tiempo de inicialización.

Además, al observar la estructura de datos, parece que su tamaño total en binarios compilados es superlineal en el número de funciones en el programa, además del tamaño de cada función.

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

VonC
fuente
2
No veo qué tiene que ver el lenguaje de implementación con eso. Necesitan utilizar bibliotecas compartidas. Algo increíble que aún no lo hagan en esta época.
Marqués de Lorne
2
@EJP: ¿Por qué necesitan usar bibliotecas compartidas?
Flimzy
9
@EJP, parte de la simplicidad de Go radica en no utilizar bibliotecas compartidas. De hecho, Go no tiene ninguna dependencia en absoluto, utiliza llamadas al sistema simples. Simplemente implemente un solo binario y simplemente funciona. Dañaría significativamente el lenguaje y su ecosistema si fuera de otra manera.
creker
10
Un aspecto a menudo olvidado de tener binarios vinculados estáticamente es que hace posible ejecutarlos en un contenedor Docker completamente vacío. Desde el punto de vista de la seguridad, esto es ideal. Cuando el contenedor está vacío, es posible que pueda ingresar (si el binario vinculado estáticamente tiene fallas), pero como no hay nada que encontrar en el contenedor, el ataque se detiene allí.
Joppe