performance - compiler - interpreter programming
¿Cómo compila Go tan rápido? (11)
Análisis de dependencia.
Desde las preguntas frecuentes de Go :
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 los archivos y bibliotecas de estilo C.
Esa es la razón principal de la rápida compilación. Y esto es por diseño.
Busqué en Google y hurgué 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 carecen de ellos), un compilador altamente optimizado o algo más? No estoy tratando de promover Go; Tengo curiosidad.
Citando el libro " The Go Programming Language " de Alan Donovan y Brian Kernighan:
Ir compilación es notablemente más rápido 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 deben enumerarse explícitamente al principio de cada archivo de origen, para que el compilador no tenga que leer y procesar un archivo completo para determinar sus dependencias. Segundo, 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 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.
Creo que Go fue diseñado en paralelo con la creación del compilador, por lo que eran mejores amigos desde el nacimiento. (OMI)
Creo que no es que los compiladores Go sean rápidos , sino que otros compiladores son lentos .
Los compiladores de C y C ++ tienen que analizar enormes cantidades de encabezados; por ejemplo, la compilación de C ++ "hello world" requiere la compilación de líneas de código de 18k, ¡que es casi la mitad de un megabyte de fuentes!
$ cpp hello.cpp | wc
18364 40513 433334
Los compiladores de Java y C # se ejecutan en una máquina virtual, lo que significa que antes de que puedan compilar cualquier cosa, el sistema operativo tiene que cargar la máquina virtual completa, luego tienen que compilarse JIT desde el código de bytes al código nativo, todo lo cual lleva algún tiempo.
La velocidad de compilación depende de varios factores.
Algunos lenguajes están diseñados para ser compilados rápidamente. Por ejemplo, Pascal fue diseñado para ser compilado usando un compilador de un solo paso.
Los compiladores en sí pueden ser optimizados también. 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 modernos de Pascal (por ejemplo, FreePascal) son más rápidos que los compiladores Go.
Go fue diseñado para ser rápido, y se nota.
- Gestión de dependencias: sin archivo de cabecera, solo necesita mirar los paquetes que se importan directamente (no hay que preocuparse por lo que importan), por lo tanto, tiene dependencias lineales.
- Gramática: la gramática del lenguaje es simple, por lo que se analiza fácilmente. Aunque el número de características se reduce, el código del compilador en sí mismo es limitado (pocas rutas).
- No se permite la sobrecarga: ve un símbolo, sabe a qué método se refiere.
- 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 estas características (los módulos son la norma en los idiomas modernos), pero lo hicieron bien.
Hay varias razones por las que 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 de 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ñados 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 lenguaje 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 típico de C / C ++, la optimización pasa al trabajo en una representación interna del código fuente original. La sobrecarga adicional en el compilador de C / C ++ viene 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á revisando todo el código de ensamblaje utilizado y quizás también esté realizando otras acciones adicionales que C / C ++ los enlazadores no están haciendo
Algunos compiladores C / C ++ (GCC) generan instrucciones en forma de texto (que se pasan al ensamblador), mientras que el compilador Go genera instrucciones en forma binaria. Se necesita hacer trabajo adicional (pero no mucho) para transformar el texto en binario.
El compilador Go apunta solo a una pequeña cantidad de arquitecturas de CPU, mientras que el compilador GCC apunta a una gran cantidad de CPU
Los compiladores que fueron diseñados con el objetivo de 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).
La eficiencia de la compilación fue un gran objetivo de diseño:
Finalmente, está pensado para ser rápido: debería llevar, como mucho, unos segundos construir un ejecutable grande en una sola computadora. Para cumplir con estos objetivos, es necesario abordar una serie de problemas lingüísticos: un sistema de tipo expresivo pero ligero; concurrencia y recolección de basura; especificación de dependencia rígida; y así. FAQ
Las preguntas frecuentes sobre el idioma son bastante interesantes con respecto a las características específicas del idioma relacionadas con el análisis:
Segundo, el lenguaje ha sido diseñado para ser fácil de analizar y se puede analizar sin una tabla de símbolos.
La idea básica de la compilación es en realidad muy simple. Un analizador de descenso recursivo, en principio, puede ejecutarse a velocidad de límite 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 mucho cálculo.
Sin embargo, no es difícil ralentizar un compilador.
Si hay una fase de preprocesador, con directivas de inclusión multinivel, definiciones de macros y compilación condicional, por más útiles que sean, no es difícil cargarlas. (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 a la cantidad de procesamiento que se puede agregar a esa fase.
Si bien la mayor parte de lo anterior es cierto, hay un punto muy importante que no se mencionó realmente: la administración de la dependencia.
Go solo necesita incluir los paquetes que está importando directamente (ya que aquellos que ya importaron lo que necesitan). Esto está en marcado contraste con C / C ++, donde cada archivo comienza con x encabezados, que incluyen encabezados y, etc. En pocas palabras: la compilación de Go lleva un tiempo lineal al número de paquetes importados, donde C / C ++ toma un tiempo exponencial.
Simplemente (en mis propias palabras), porque la sintaxis es muy fácil (para analizar y analizar)
Por ejemplo, ninguna herencia de tipo significa, no es un 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 comprueba 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á utilizando (o si se supone que debe mantener un valor de retorno y no lo está)
Lo siguiente no compila:
package main
func main() {
var a int
a = 0
}
notused.go:3: a declared and not used
Este tipo de cumplimiento y principles hacen que el código resultante sea más seguro, y el compilador no tiene que realizar validaciones adicionales que el programador pueda hacer.
En general, todos estos detalles hacen que un lenguaje sea más fácil de analizar, lo que da como resultado compilaciones rápidas.
De nuevo, en mis propias palabras.
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 de 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:
Una sintaxis claramente definida que es matemáticamente sólida, para un análisis y análisis eficientes.
Un lenguaje de tipo seguro y compilado estáticamente que usa una compilación separada con dependencia y verificación de tipos a través de los límites de los módulos, para evitar la lectura innecesaria de los archivos de cabecera 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ódulos cruzados (de ahí la necesidad de volver a leer todos esos archivos de encabezado una y otra vez, incluso para un programa sencillo de "una línea de" hola mundo ").
Una implementación eficiente del compilador (por ejemplo, análisis de arriba hacia abajo, descendente recursivo descendente), que por supuesto recibe una gran ayuda en los puntos 1 y 2 anteriores.
Estos principios ya se conocieron y se implementaron en su totalidad en las décadas de 1970 y 1980 en idiomas como Mesa, Ada, Modula-2 / Oberon y varios otros, y solo ahora (en la década de 2010) están ingresando en idiomas modernos como Go (Google) , Swift (Apple), C # (Microsoft) y varios otros.
Esperemos que esta sea pronto la norma y no la excepción. Para llegar allí, dos cosas deben suceder:
Primero, los proveedores de plataformas de software como Google, Microsoft y Apple deberían comenzar por alentar a los desarrolladores de aplicaciones a usar la nueva metodología de compilación, mientras les permite reutilizar su base de código existente. Esto es lo que Apple está tratando de hacer con el lenguaje de programación Swift, que puede coexistir con Objective-C (ya que utiliza el mismo entorno de ejecución).
En segundo lugar, las propias plataformas de software subyacentes deberían reescribirse con el tiempo utilizando estos principios, mientras que al mismo tiempo rediseñan la jerarquía de módulos en el proceso para hacerlos menos monolíticos. Esta es, por supuesto, una tarea gigantesca y bien puede durar la mayor parte de una década (si son lo suficientemente valientes para hacerlo, lo cual no estoy seguro en absoluto en el caso de Google).
En cualquier caso, es la plataforma 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 cotización es para una placa de desarrollo FPGA Xartinx Spartan-3 de bajo costo que se ejecuta a una frecuencia de reloj de 25 MHz y que cuenta con 1 MB de memoria principal. De este modo, se puede extrapolar fácilmente a "menos de 1 segundo" para un procesador moderno que funciona 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 teniendo en cuenta las velocidades de E / S. Ya en 1990, cuando Oberon se ejecutaba en un procesador NS32X32 de 25MHz con 2-4 MBytes de memoria principal, el compilador se compilaba solo en unos pocos segundos. La noción de esperar realmente a que el compilador terminara un ciclo de compilación era completamente desconocida para los programadores de Oberon incluso en aquel entonces. Para los programas típicos, siempre tomó 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 se acaba de activar. Fue realmente una gratificación instantánea, con tiempos de espera cercanos a cero. Y la calidad del código producido, aunque no siempre completamente a la par con los mejores compiladores disponibles en ese entonces, fue notablemente buena para la mayoría de las tareas y bastante aceptable en general.