¿Cómo se compila Go tan rápido?

217

Busqué en Google y busqué en el sitio web de Go, pero parece que no puedo encontrar una explicación para los extraordinarios tiempos de construcción de Go. ¿Son productos de las características del lenguaje (o la falta de ellas), un compilador altamente optimizado u otra cosa? No estoy tratando de promocionar Go; Tengo curiosidad.

Evan Kroske
fuente
12
@ Apoyo, soy consciente de eso. Creo que implementar un compilador de tal manera que se compile con notable rapidez no es una optimización prematura. Es más que probable que represente el resultado de buenas prácticas de diseño y desarrollo de software. Además, no puedo soportar ver las palabras de Knuth sacadas de contexto y aplicadas incorrectamente.
Adam Crossland
55
La versión pesimista de esta pregunta es "¿Por qué C ++ compila tan lentamente?" stackoverflow.com/questions/588884/…
dan04
14
He votado para reabrir esta pregunta, ya que no está basada en opiniones. Se puede dar una buena descripción técnica (no obstinada) de las opciones de lenguaje y / o compilador que facilitan la velocidad de compilación.
Martin Tournoij
Para proyectos pequeños, Go me parece lento. Esto se debe a que recuerdo que Turbo-Pascal era mucho más rápido en una computadora que probablemente era miles de veces más lenta. prog21.dadgum.com/47.html?repost=true . Cada vez que escribo "ir a construir" y no pasa nada durante varios segundos, pienso en los viejos compiladores de Fortran y las tarjetas perforadas. YMMV. TLDR: "lento" y "rápido" son términos relativos.
RedGrittyBrick
Definitivamente recomiendo leer dave.cheney.net/2014/06/07/five-things-that-make-go-fast para obtener información más detallada
Karthik

Respuestas:

193

Análisis de dependencia.

Las preguntas frecuentes de Go solían contener la siguiente oración:

Go proporciona un modelo para la construcción de software que facilita el análisis de dependencia y evita gran parte de la sobrecarga de incluir archivos y bibliotecas de estilo C.

Si bien la frase ya no se encuentra en las preguntas frecuentes, este tema se desarrolla en la charla Go en Google , que compara el enfoque de análisis de dependencia de C / C ++ y Go.

Esa es la razón principal de la compilación rápida. Y esto es por diseño.

Igor Krivokon
fuente
Esta frase ya no se encuentra en las Preguntas frecuentes sobre Go, pero una explicación más detallada del tema "análisis de dependencia" que compara el enfoque C / C ++ y Pascal / Modula / Go está disponible en la charla Go en Google
rob74
76

Creo que no es que los compiladores de Go sean rápidos , es que otros compiladores son lentos .

Los compiladores C y C ++ tienen que analizar enormes cantidades de encabezados; por ejemplo, compilar C ++ "hello world" requiere compilar 18 mil líneas de código, ¡lo que equivale a casi medio megabyte de fuentes!

$ cpp hello.cpp | wc
  18364   40513  433334

Los compiladores Java y C # se ejecutan en una VM, lo que significa que antes de que puedan compilar algo, el sistema operativo debe cargar toda la VM, luego deben compilarse JIT desde el código de bytes hasta el código nativo, todo lo cual lleva algún tiempo.

La velocidad de compilación depende de varios factores.

Algunos idiomas están diseñados para compilarse rápidamente. Por ejemplo, Pascal fue diseñado para ser compilado usando un compilador de un solo paso.

Los compiladores también se pueden optimizar. Por ejemplo, el compilador Turbo Pascal fue escrito en un ensamblador optimizado a mano que, combinado con el diseño del lenguaje, resultó en un compilador realmente rápido que funciona en hardware de clase 286. Creo que incluso ahora, los compiladores Pascal modernos (por ejemplo, FreePascal) son más rápidos que los compiladores Go.

el.pescado
fuente
19
El compilador C # de Microsoft no se ejecuta en una VM. Todavía está escrito en C ++, principalmente por razones de rendimiento.
blucz
19
Turbo Pascal y más tarde Delphi son los mejores ejemplos para compiladores increíblemente rápidos. Después de que el arquitecto de ambos ha migrado a Microsoft, hemos visto grandes mejoras tanto en los compiladores de MS como en los idiomas. Esa no es una coincidencia aleatoria.
TheBlastOne
77
18k líneas (18364 para ser exactos) de código son 433334 bytes (~ 0,5MB)
el.pescado
99
El compilador de C # se ha compilado con C # desde 2011. Solo una actualización en caso de que alguien lea esto más tarde.
Kurt Koller
3
Sin embargo, el compilador de C # y el CLR que ejecuta el MSIL generado son cosas diferentes. Estoy bastante seguro de que el CLR no está escrito en C #.
Jocull
39

Existen múltiples razones por las cuales el compilador Go es mucho más rápido que la mayoría de los compiladores C / C ++:

  • Razón principal : la mayoría de los compiladores C / C ++ exhiben diseños excepcionalmente malos (desde la perspectiva de la velocidad de compilación). Además, desde la perspectiva de la velocidad de compilación, algunas partes del ecosistema C / C ++ (como los editores en los que los programadores escriben sus códigos) no están diseñadas teniendo en cuenta la velocidad de compilación.

  • Razón principal : la rápida velocidad de compilación fue una elección consciente en el compilador Go y también en el idioma Go

  • El compilador Go tiene un optimizador más simple que los compiladores C / C ++

  • A diferencia de C ++, Go no tiene plantillas ni funciones en línea. Esto significa que Go no necesita realizar ninguna instancia de plantilla o función.

  • El compilador Go genera un código de ensamblaje de bajo nivel antes y el optimizador funciona en el código de ensamblaje, mientras que en un compilador C / C ++ típico, la optimización pasa a trabajar en una representación interna del código fuente original. La sobrecarga adicional en el compilador C / C ++ proviene del hecho de que la representación interna necesita ser generada.

  • La vinculación final (5l / 6l / 8l) de un programa Go puede ser más lenta que la vinculación de un programa C / C ++, porque el compilador Go está pasando por todo el código de ensamblaje utilizado y tal vez también está haciendo otras acciones adicionales que C / C ++ los enlazadores no están haciendo

  • Algunos compiladores C / C ++ (GCC) generan instrucciones en forma de texto (para pasar al ensamblador), mientras que el compilador Go genera instrucciones en forma binaria. Es necesario realizar un trabajo adicional (pero no mucho) para transformar el texto en binario.

  • El compilador Go apunta solo a un pequeño número de arquitecturas de CPU, mientras que el compilador GCC apunta a un gran número de CPU

  • Los compiladores que fueron diseñados con el objetivo de una alta velocidad de compilación, como Jikes, son rápidos. En una CPU de 2 GHz, Jikes puede compilar más de 20000 líneas de código Java por segundo (y el modo incremental de compilación es aún más eficiente).

usuario811773
fuente
17
El compilador de Go incorpora pequeñas funciones. No estoy seguro de cómo apuntar a un pequeño número de CPU te hace más rápido, más lento ... Supongo que gcc no genera código PPC mientras estoy compilando para x86.
Brad Fitzpatrick
@BradFitzpatrick odia resucitar un comentario anterior, pero al apuntar a un número menor de plataformas, los desarrolladores del compilador pueden pasar más tiempo optimizándolo para cada uno.
Persistencia
El uso de un formulario intermedio le permite admitir muchas más arquitecturas ya que ahora solo tiene que escribir un nuevo back-end para cada nueva arquitectura
phuclv
34

La eficiencia de la compilación fue un objetivo de diseño importante:

Finalmente, se pretende que sea rápido: debería tomar como máximo unos segundos construir un gran ejecutable en una sola computadora. Para cumplir con estos objetivos fue necesario abordar una serie de cuestiones lingüísticas: un sistema de tipo expresivo pero ligero; concurrencia y recolección de basura; especificación de dependencia rígida; y así. Preguntas más frecuentes

Las preguntas frecuentes sobre el lenguaje son bastante interesantes con respecto a las características específicas del lenguaje relacionadas con el análisis:

Segundo, el lenguaje ha sido diseñado para ser fácil de analizar y puede analizarse sin una tabla de símbolos.

Larry OBrien
fuente
66
Eso no es cierto. No puede analizar completamente el código fuente de Go sin una tabla de símbolos.
12
Tampoco veo por qué la recolección de basura mejora los tiempos de compilación. Simplemente no lo hace.
TheBlastOne
3
Estas son citas de las preguntas frecuentes: golang.org/doc/go_faq.html No puedo decir si no lograron sus objetivos (tabla de símbolos) o si su lógica es defectuosa (GC).
Larry OBrien
55
@FUZxxl Vaya a golang.org/ref/spec#Primary_expressions y considere las dos secuencias [Operando, Llamada] y [Conversión]. Ejemplo Código fuente de Go: identificador1 (identificador2). Sin una tabla de símbolos, es imposible decidir si este ejemplo es una llamada o una conversión. El | Cualquier lenguaje puede analizarse hasta cierto punto sin una tabla de símbolos. Es cierto que la mayoría de las partes de los códigos fuente de Go se pueden analizar sin una tabla de símbolos, pero no es cierto que sea posible reconocer todos los elementos de gramática definidos en las especificaciones de Golang.
3
@Atom Usted trabaja duro para evitar que el analizador sea el código que informa un error. Los analizadores generalmente hacen un mal trabajo al informar mensajes de error coherentes. Aquí, crea un árbol de análisis para la expresión como si fuera aTypeuna referencia variable, y más adelante en la fase de análisis semántico cuando descubre que no está imprimiendo un error significativo en ese momento.
Sam Harwell
26

Si bien la mayor parte de lo anterior es cierto, hay un punto muy importante que no se mencionó realmente: la gestión de dependencias.

Go solo necesita incluir los paquetes que está importando directamente (como los que ya importaron lo que necesitan). Esto está en marcado contraste con C / C ++, donde cada archivo comienza, incluidos los encabezados x, que incluyen los encabezados y, etc.

Kosta
fuente
22

Una buena prueba para la eficiencia de traducción de un compilador es la autocompilación: ¿cuánto tiempo tarda un compilador determinado en compilarse? Para C ++ lleva mucho tiempo (¿horas?). En comparación, un compilador Pascal / Modula-2 / Oberon se compilaría en menos de un segundo en una máquina moderna [1].

Go se ha inspirado en estos idiomas, pero algunas de las razones principales de esta eficiencia incluyen:

  1. Una sintaxis claramente definida que es matemáticamente sólida, para un escaneo y análisis eficiente.

  2. Un lenguaje compilado de forma segura y estático que utiliza compilación separada con dependencia y verificación de tipo a través de los límites del módulo, para evitar la relectura innecesaria de los archivos de encabezado y la compilación de otros módulos, en lugar de la compilación independiente como en C / C ++ donde el compilador no realiza tales comprobaciones de módulo cruzado (de ahí la necesidad de volver a leer todos esos archivos de encabezado una y otra vez, incluso para un simple programa "hello world" de una línea).

  3. Una implementación eficiente del compilador (p. Ej., Análisis de arriba a abajo recursivo de un solo paso), que por supuesto es de gran ayuda en los puntos 1 y 2 anteriores.

Estos principios ya se conocían y se implementaron por completo en los años setenta y ochenta en idiomas como Mesa, Ada, Modula-2 / Oberon y varios otros, y solo ahora (en los años 2010) están llegando a idiomas modernos como Go (Google) , Swift (Apple), C # (Microsoft) y varios otros.

Esperemos que esto sea pronto la norma y no la excepción. Para llegar allí, deben suceder dos cosas:

  1. Primero, los proveedores de plataformas de software como Google, Microsoft y Apple deberían comenzar alentando a los desarrolladores de aplicaciones a usar la nueva metodología de compilación, mientras les permiten reutilizar su base de código existente. Esto es lo que Apple ahora está tratando de hacer con el lenguaje de programación Swift, que puede coexistir con Objective-C (ya que usa el mismo entorno de tiempo de ejecución).

  2. En segundo lugar, las plataformas de software subyacentes deberían reescribirse eventualmente con el tiempo utilizando estos principios, mientras se rediseña simultáneamente la jerarquía de módulos en el proceso para hacerlos menos monolíticos. Por supuesto, esta es una tarea gigantesca y bien puede llevar la mayor parte de una década (si son lo suficientemente valientes como para hacerlo realmente, lo cual no estoy del todo seguro en el caso de Google).

En cualquier caso, es la plataforma la que impulsa la adopción del lenguaje, y no al revés.

Referencias

[1] http://www.inf.ethz.ch/personal/wirth/ProjectOberon/PO.System.pdf , página 6: "El compilador se compila en unos 3 segundos". Esta cita es para una placa de desarrollo Xilinx Spartan-3 FPGA de bajo costo que funciona a una frecuencia de reloj de 25 MHz y presenta 1 MByte de memoria principal. A partir de este, se puede extrapolar fácilmente a "menos de 1 segundo" para un procesador moderno que funcione a una frecuencia de reloj muy superior a 1 GHz y varios GBytes de memoria principal (es decir, varios órdenes de magnitud más potentes que la placa FPGA Xilinx Spartan-3), incluso cuando se tienen en cuenta las velocidades de E / S. Ya en 1990, cuando Oberon se ejecutaba en un procesador NS32X32 de 25MHz con 2-4 MB de memoria principal, el compilador se compiló en solo unos segundos. La noción de esperar realmentepara el programador terminar un ciclo de compilación era completamente desconocido para los programadores de Oberon incluso en aquel entonces. Para los programas típicos, siempre lleva más tiempo quitar el dedo del botón del mouse que activó el comando de compilación que esperar a que el compilador complete la compilación que acaba de activarse. Fue realmente una gratificación instantánea, con tiempos de espera cercanos a cero. Y la calidad del código producido, aunque no siempre estuvo completamente a la par con los mejores compiladores disponibles en ese momento, fue notablemente buena para la mayoría de las tareas y bastante aceptable en general.

Andreas
fuente
1
Un compilador Pascal / Modula-2 / Oberon / Oberon-2 se compilaría en menos de un segundo en una máquina moderna [cita requerida]
CoffeeandCode
1
Cita añadida, ver referencia [1].
Andreas
1
"... principios ... encontrando su camino en lenguajes modernos como Go (Google), Swift (Apple)" No estoy seguro de cómo Swift entró en esa lista: el compilador Swift es glacial . En una reunión reciente de CocoaHeads Berlin, alguien proporcionó algunos números para un marco de tamaño mediano, llegaron a 16 LOC por segundo.
mpw
13

Go fue diseñado para ser rápido, y se nota.

  1. Gestión de dependencias: no hay archivo de encabezado, solo necesita mirar los paquetes que se importan directamente (no necesita preocuparse por lo que importan), por lo tanto, tiene dependencias lineales.
  2. Gramática: la gramática del lenguaje es simple, por lo tanto, se analiza fácilmente. Aunque el número de características se reduce, el código del compilador en sí es limitado (pocas rutas).
  3. No se permite sobrecarga: ve un símbolo, sabe a qué método se refiere.
  4. Es trivialmente posible compilar Go en paralelo porque cada paquete se puede compilar de forma independiente.

Tenga en cuenta que GO no es el único idioma con tales características (los módulos son la norma en los idiomas modernos), pero lo hicieron bien.

Matthieu M.
fuente
El punto (4) no es del todo cierto. Los módulos que dependen unos de otros deben compilarse en orden de dependencia para permitir la incorporación de módulos cruzados y otras cosas.
fuz
1
@FUZxxl: Sin embargo, esto solo concierne a la etapa de optimización, puede tener un paralelismo perfecto hasta la generación de IR de back-end; Por lo tanto, solo se trata de la optimización de módulos cruzados, que se puede hacer en la etapa de enlace, y el enlace no es paralelo de todos modos. Por supuesto, si no desea duplicar su trabajo (volver a analizar), es mejor que compile de forma "reticular": 1 / módulos sin dependencia, 2 / módulos que dependen solo de (1), 3 / módulos dependiendo solo de (1) y (2), ...
Matthieu M.
2
Lo cual es perfectamente fácil de hacer utilizando utilidades básicas como un Makefile.
fuz
12

Citando el libro " The Go Programming Language " de Alan Donovan y Brian Kernighan:

La compilación de Go es notablemente más rápida que la mayoría de los otros lenguajes compilados, incluso cuando se construye desde cero. Hay tres razones principales para la velocidad del compilador. Primero, todas las importaciones se deben enumerar explícitamente al comienzo de cada archivo fuente, de modo que el compilador no tenga que leer y procesar un archivo completo para determinar sus dependencias. En segundo lugar, las dependencias de un paquete forman un gráfico acíclico dirigido y, como no hay ciclos, los paquetes se pueden compilar por separado y quizás en paralelo. Finalmente, el archivo de objeto para un paquete Go compilado registra información de exportación no solo para el paquete en sí, sino también para sus dependencias. Al compilar un paquete, el compilador debe leer un archivo de objeto para cada importación, pero no necesita mirar más allá de estos archivos.

Sinvergüenza
fuente
9

La idea básica de compilación es realmente muy simple. Un analizador de descenso recursivo, en principio, puede funcionar a velocidad de E / S. La generación de código es básicamente un proceso muy simple. Una tabla de símbolos y un sistema de tipo básico no es algo que requiera muchos cálculos.

Sin embargo, no es difícil ralentizar un compilador.

Si hay una fase de preprocesador, con directivas de inclusión de múltiples niveles , definiciones de macro y compilación condicional, tan útiles como esas cosas son, no es difícil cargarlo. (Por ejemplo, estoy pensando en los archivos de encabezado de Windows y MFC). Es por eso que los encabezados precompilados son necesarios.

En términos de optimización del código generado, no hay límite para la cantidad de procesamiento que se puede agregar a esa fase.

Mike Dunlavey
fuente
7

Simplemente (en mis propias palabras), porque la sintaxis es muy fácil (analizar y analizar)

Por ejemplo, no significa herencia de tipo, no análisis problemático para averiguar si el nuevo tipo sigue las reglas impuestas por el tipo base.

Por ejemplo, en este ejemplo de código: "interfaces" el compilador no va y verifica si el tipo deseado implementa la interfaz dada mientras analiza ese tipo. Solo hasta que se use (y SI se usa) se realiza la verificación.

Otro ejemplo, el compilador le dice si está declarando una variable y no la está usando (o si se supone que tiene un valor de retorno y no lo está)

Lo siguiente no se compila:

package main
func main() {
    var a int 
    a = 0
}
notused.go:3: a declared and not used

Este tipo de aplicación y principios hacen que el código resultante sea más seguro, y el compilador no tiene que realizar validaciones adicionales que el programador puede hacer.

En general, todos estos detalles hacen que un lenguaje sea más fácil de analizar, lo que resulta en compilaciones rápidas.

De nuevo, en mis propias palabras.

revs OscarRyz
fuente
3

Creo que Go fue diseñado en paralelo con la creación del compilador, por lo que fueron mejores amigos desde el nacimiento. (OMI)

Andrey
fuente
0
  • Go importa dependencias una vez para todos los archivos, por lo que el tiempo de importación no aumenta exponencialmente con el tamaño del proyecto.
  • Una lingüística más simple significa que interpretarlos requiere menos computación.

¿Qué más?

Alberto Salvia Novella
fuente