El directorio es el siguiente:
-
Conoce ir a construir
-
Principios del compilador
-
Análisis léxico
-
Análisis gramatical
-
Análisis semántico
-
Generación de código intermedio
-
Optimización de código
-
Generación de código de máquina
-
Resumen
Conoce ir a construir
Cuando Qiaoxia go build
tiempo, se escribe archivos de código fuente en realidad experimentó lo que las cosas? Finalmente se convirtió en un archivo ejecutable.
Este comando compilará el código go. ¡Echemos un vistazo al proceso de compilación de go today!
En primer lugar, conozcamos la clasificación del archivo fuente del código de go
-
Archivo fuente de comando: en resumen, el archivo que contiene la función principal, generalmente un archivo por proyecto, y nunca he pensado en un proyecto que requiera dos archivos fuente de comando
-
archivo de fuente de prueba: que escribimos unidad de código de prueba es a
_test.go
finales -
Archivos de código fuente de la biblioteca: los archivos de código fuente de la biblioteca sin las características anteriores, como muchos paquetes de terceros que utilizamos pertenecen a esta parte
go build
Este comando se utiliza para compilar uno de los archivos de origen de mando , y depende de los archivos de origen de la biblioteca . La siguiente tabla es un resumen de algunas opciones de uso común.
Opcional | Explicación |
---|---|
-una | Reconstruya todos los archivos fuente de comandos y los archivos fuente de la biblioteca, incluso los últimos |
-norte | Imprima todos los comandos involucrados en la compilación, pero no se ejecutarán, lo cual es muy conveniente para nosotros para aprender |
-carrera | Permite la detección de condiciones de carrera, las plataformas compatibles son limitadas |
-X | Imprima el nombre utilizado durante la compilación, la diferencia entre él y -n es que no solo imprime sino que también se ejecuta |
A continuación, use un programa hello world para demostrar las opciones de comando anteriores.
Si el código para llevar a cabo lo anterior go build-n
nos fijamos en la salida:
Analizar todo el proceso de ejecución.
Esta parte es el núcleo de la compilación compile
, buildid
y link
el archivo ejecutable será compilado por los tres comandos a.out
.
Entonces mv
ordenó el traslado a.out a la carpeta actual, y cambiar el archivo de proyecto con el mismo nombre (donde también se puede especificar el nombre de su preferencia).
Más adelante en el artículo, estamos hablando principalmente es decir compile
, buildid、
link
estos tres comandos que intervienen en el proceso de compilación.
Principios del compilador
Esta es la ruta del código fuente del compilador go: https://github.com/golang/go/tree/master/src/cmd/compile
Como puede ver en la imagen de arriba, todo el compilador se puede dividir en: compilación front-end y compilación back-end; ahora veamos qué hace el compilador en cada etapa. Comencemos con el frente.
Análisis léxico
El análisis léxico es simplemente traducir el código fuente en el que escribimos Token
. ¿Qué significa esto?
Para entender la Golang
traducción del código fuente para el Token
proceso, nos fijamos en una pieza de un solo código traducido para una misma situación.
Figura lugares importantes que se han anotado, pero todavía hay un par de palabras para decir lo que nos fijamos en el código Imagínese, si desea que nuestro propio para alcanzar esta "traducción" y cómo el programa debe reconocer Token
que?
Primero, clasifiquemos primero los tipos de tokens de Go: nombres de variables, literales, operadores, separadores y palabras clave. Necesitamos dividir un montón de código fuente de acuerdo con las reglas, que en realidad es la segmentación de palabras. Mirando el código de ejemplo anterior, podemos formular una regla de la siguiente manera:
-
Identifica el espacio, si es un espacio, puedes dividir una palabra;
-
Encuentro
(
,)
'<', '>', etc. Estos operadores especiales cuando un número de palabras; -
Encuentro "o participio de medida literal numérico.
Por simple análisis anterior, podemos ver el código fuente, de hecho, a su vez, Token
en realidad no es muy complicado, puede escribir su propio código para alcanzarlos. Por supuesto, hay muchos analizador léxico más común de lograr a través de la manera regular, al igual que el Golang
uso temprano es lex
, en una versión posterior antes de cambiar a la utilización por recorrer para lograr su propia cuenta.
Análisis gramatical
Tras el análisis léxico, obtenemos que la Token
secuencia, que servirá como la entrada del analizador. Luego, después del proceso de generación de AST
la estructura como de salida.
El llamado análisis de sintaxis es para Token
ser convertido a un programa reconocido por la estructura gramatical, pero AST
es esta una representación abstracta de gramática. Hay dos formas de construir este árbol.
-
Este enfoque de arriba hacia abajo en primer lugar construir el nodo raíz, y luego iniciar la exploración
Token
, la caraSTRING
o de otros tipos saben que esto está haciendo el tipo indicado,func
significa que la función se declara. Simplemente siga escaneando hasta el final del programa. -
El enfoque ascendente es lo opuesto al enfoque anterior: primero construye el subárbol y luego lo ensambla en un árbol completo.
ir lenguaje de análisis utilizando el enfoque de abajo hacia arriba para construir AST
, aquí vamos vistazo a la lengua por Token
la estructura de árbol se parece.
He marcado todas las partes interesantes en el texto. Usted encontrará que cada AST
nodo del árbol se asocia con una Token
ubicación física correspondiente.
Después de construir el árbol, podemos ver que los diferentes tipos están representados por las estructuras correspondientes. Si hay errores gramaticales o léxicos aquí, no se resolverán. Porque hasta ahora, todo es procesamiento de cadenas.
Análisis semántico
Etapa dentro de la relación de sintaxis compilador después de que el análisis se llama análisis semántico , e ir en esta etapa se llama la verificación de tipos , pero miré a seguir sus propios documentos, de hecho, no tienen mucha diferencia, seguimos la especificación de la corriente principal para escribir este Proceso.
Entonces, ¿qué hace exactamente el análisis semántico (verificación de tipo)?
AST
Después de la generación, el análisis semántico lo usará como entrada, y algunas operaciones relacionadas también se reescribirán directamente en este árbol.
El primero se Golang
menciona en la comprobación de la documentación tipo, así como la inferencia de tipos, para ver el tipo de partidos, si la conversión implícita (ir sin conversión implícita). Como dice el siguiente texto:
El AST luego se verifica por tipo. Los primeros pasos son la resolución de nombres y la inferencia de tipos, que determinan qué objeto pertenece a qué identificador y qué tipo tiene cada expresión. La verificación de tipo incluye ciertas verificaciones adicionales, como "declarada y no utilizada", así como determinar si una función termina o no.
La idea principal es: después de que se genera el AST, la verificación de tipos (es decir, el análisis semántico del que estamos hablando aquí), el primer paso es realizar la verificación de nombres y la inferencia de tipos, firmar el identificador al que pertenece cada objeto y de qué tipo tiene cada expresión. La verificación de tipo también tiene que hacer otras verificaciones, como "declarar no utilizado" y determinar si la función se cancela.
Ciertas transformaciones también se realizan en el AST. Algunos nodos se refinan en función de la información de tipo, como las adiciones de cadenas que se dividen del tipo de nodo de adición aritmética. Algunos otros ejemplos son la eliminación de código muerto, la función de llamada en línea y el análisis de escape.
Este párrafo dice: AST también se convertirá, y algunos nodos se simplificarán de acuerdo con la información de tipo, como dividir la suma de cadenas del tipo de nodo de suma aritmética. Otros ejemplos son la eliminación del código muerto, la inclusión de llamadas a funciones y el análisis de escape.
Los dos párrafos anteriores son de compilación de golang: https://github.com/golang/go/tree/master/src/cmd/compile
Una cosa más que decir aquí, a menudo necesitamos prohibir la inclusión en línea al depurar código, que en realidad es esta etapa de operación.
1. `# Prohibir la restricción durante la compilación` 2.` ir a construir -gcflags '-N -l'' 4.` -N Prohibir la compilación y optimización` 5.` -l Prohibir la inserción, la desactivación de la inserción también puede ser en cierta medida Reducir el tamaño del programa ejecutable
Después del análisis semántico, se puede demostrar que nuestra estructura de código y gramática no son un problema. Entonces, el extremo frontal del compilador es principalmente analizar la estructura AST correcta que el extremo posterior del compilador puede manejar.
A continuación, veamos qué debe hacer el backend del compilador.
La máquina solo puede entender el binario y ejecutarlo, por lo que la tarea del backend del compilador es simplemente cómo traducir AST al código de la máquina.
Generación de código intermedio
Ahora que se ha obtenido el AST, el binario requerido para la operación de la máquina. ¿Por qué no traducir directamente a binario? De hecho, hasta ahora técnicamente, no hay ningún problema en absoluto.
Sin embargo, tenemos una variedad de sistemas operativos con diferentes tipos de CPU, cada uno de los cuales puede tener un número diferente de bits; las instrucciones que los registros pueden usar también son diferentes, como conjuntos de instrucciones complejos y conjuntos de instrucciones reducidos; Antes de la compatibilidad, también necesitamos reemplazar algunas funciones de bajo nivel. Por ejemplo, usamos make para inicializar el segmento. En este momento, será reemplazado por: makeslice64
o de acuerdo con el tipo pasado makeslice
. Por supuesto, el reemplazo de funciones como el dolor, el canal, etc. también será reemplazado durante el proceso de generación de código intermedio. Esta parte de la operación de reemplazo se puede ver aquí
Otro valor del código intermedio es mejorar la reutilización de la compilación de back-end. Por ejemplo, hemos definido cómo debería ser un conjunto de código intermedio, por lo que la generación de código de máquina de back-end es relativamente fija. Cada idioma solo necesita completar su propio trabajo de compilación front-end. Es por eso que ahora puede ver la velocidad de desarrollar un nuevo lenguaje. La compilación es en su mayoría reutilizable.
Y para el próximo trabajo de optimización, la existencia de código intermedio tiene una importancia extraordinaria. Debido a que hay tantas plataformas, si hay un código intermedio, podemos poner algunas optimizaciones comunes aquí.
El código intermedio es también una variedad de formatos, tales como Golang
el uso del código intermedio es las características de la SSA (IR), esta forma de código intermedio, el más característico es las variables más importantes siempre se definen antes de usar variables, y cada variable sólo Asignar una vez.
Optimización de código
En la documentación de compilación de go, no encontré un paso independiente para optimizar el código. Sin embargo, de acuerdo con nuestro análisis anterior, podemos ver que el proceso de optimización del código se encuentra en cada etapa del compilador. Todos harán algo dentro de su poder.
En general, además de reemplazar los ineficientes con códigos eficientes, también tenemos los siguientes tratamientos:
-
Paralelismo, aproveche al máximo las características de las computadoras multinúcleo actuales
-
Pipeline, cpu a veces puede procesar instrucciones b al mismo tiempo al procesar una instrucción
-
La selección de instrucciones, para que la CPU complete ciertas operaciones, las instrucciones deben usarse, pero la eficiencia de las diferentes instrucciones es muy diferente, y la optimización de las instrucciones se realizará aquí
-
Usando registros y caché, todos sabemos que la CPU toma el más rápido del registro, y el segundo del caché. Aquí será totalmente utilizado
Generación de código de máquina
El código intermedio optimizado se convertirá primero en código ensamblador (Plan9) en esta etapa, y el lenguaje ensamblador es solo una representación de texto del código de máquina, y la máquina no puede ejecutarlo realmente. Entonces, en esta etapa, se llamará al ensamblador, y el ensamblador llamará al código correspondiente para generar el código de máquina de destino de acuerdo con la arquitectura que establecimos al realizar la compilación.
Lo más interesante aquí es que Golang
siempre digo que mi ensamblador es multiplataforma. De hecho, también escribió código multipunto para traducir el código de máquina final. Porque él, en nuestro conjunto en el momento de la entrada GOARCH=xxx
para inicializar el procesamiento de parámetros, y, finalmente, llamar a un método específico para la preparación de la arquitectura correspondiente a generar código máquina. Este tipo de lógica de la capa superior es consistente y la lógica de la capa inferior es inconsistente. Es muy común y vale la pena aprenderlo. Echemos un breve vistazo a este proceso.
Primero mira la función de entrada cmd/compile/main.go:main()
1. `var archInits = map [string] func (* gc.Arch) {` 2. `" 386 ": x86.Init,` 3. `" amd64 ": amd64.Init,` 4. `" amd64p32 ": amd64.Init, `5.` " arm ": arm.Init,` 6.` "arm64": arm64.Init, `7.` " mips ": mips.Init,` 8.` "mipsle": mips. Init, ` 9.`" mips64 ": mips64.Init,` 10.` "mips64le": mips64.Init, `11.` " ppc64 ": ppc64.Init,` 12.` "ppc64le": ppc64.Init, ` 13.`" s390x ": s390x.Init,` 14.` "wasm": wasm.Init, `15.` }` 17.` func main () {` 18.` // Según los parámetros del mapa anterior Seleccione el procesamiento de la arquitectura correspondiente` 19. `archInit, ok: = archInits [objabi.GOARCH] `20.` if! Ok {` 21.` ...... ` 22.`} ` 23. `// Pasa la correspondencia de la arquitectura de CPU correspondiente al interior` 24.` gc.Main (archInit)` 25.` } `
Entonces cmd/internal/obj/plist.go
el procesamiento de método correspondiente a la llamada arquitectura
1. `func Flushplist (ctxt * Link, plist * Plist, newprog ProgAlloc, myimportpath string) (` 2. `... ...` 3. `for _, s: = range text {` 4. `mkfwd ( s) ` 5.` linkpatch (ctxt, s, newprog)` 6.` // El método de arquitectura correspondiente realiza su propia traducción de código de máquina` 7. `ctxt.Arch.Preprocess (ctxt, s, newprog)` 8. ` ctxt.Arch.Assemble (ctxt, s, newprog) ` 10.` linkpcln (ctxt, s) ` 11.` ctxt.populateDWARF (plist.Curfn, s, myimportpath) `12.` }` 13.` } ``
Después de todo el proceso, puede ver que hay mucho trabajo por hacer en el back-end del compilador. Debe comprender la arquitectura de un determinado conjunto de instrucciones y CPU para traducir el código de la máquina correctamente. Al mismo tiempo, no puede ser justo: la eficiencia de un lenguaje es alta o baja, lo que también depende en gran medida de la optimización del backend del compilador. En particular, cuando estamos a punto de entrar en la era de la IA, nacen cada vez más fabricantes de chips. Calculo que la demanda de talentos en esta área será cada vez más vigorosa en el futuro.
Resumen
Resuma algunas ganancias al aprender esta parte del antiguo conocimiento del compilador:
-
Sepa que toda la compilación consta de varias etapas y de lo que hace cada etapa, pero algunos detalles de la implementación más profunda de cada etapa no se conocen ni se pretende que sepan;
-
Incluso si se trata de un compilador tan complejo, las cosas de muy bajo nivel se pueden descomponer para que cada etapa sea independiente y se vuelva reutilizable, lo que tiene cierta importancia para mí en el desarrollo de aplicaciones;
-
La estratificación es dividir las responsabilidades, pero algunas cosas deben hacerse a nivel mundial, como la optimización, de hecho, se realizará en cada etapa; también es de cierta importancia de referencia para nuestro sistema de diseño;
-
Aprendido
Golang
muchas maneras de exposición externa es en realidad azúcar sintáctico (tales como: marca, painc etc.), el compilador me va a ayudar a traducir, pensé que era el principio del nivel de código, hazlo en tiempo de ejecución, similar al modelo de fábrica, ahora Mirarte a ti mismo es realmente ingenuo; -
Hice algunos preparativos básicos para la próxima preparación para aprender el mecanismo operativo de Go y la compilación de Plan9.