
Continuamos estudiando el compilador Swift. Esta parte está dedicada al lenguaje intermedio rápido.
Si no ha visto los anteriores, le recomiendo que siga el enlace y lea:
Silgen
El siguiente paso es convertir el AST escrito a SIL sin procesar. Swift Intermediate Language (SIL) es una representación intermedia especialmente creada para Swift. Puede encontrar una descripción de todas las instrucciones en la documentación .
SIL tiene un formulario SSA. Asignación única estática (SSA): una representación de código en la que a cada variable se le asigna un valor solo una vez. Se crea a partir del código regular agregando variables adicionales. Por ejemplo, usando un sufijo numérico que indica la versión de una variable después de cada asignación.
Gracias a este formulario, es más fácil para el compilador optimizar el código. A continuación se muestra un ejemplo de pseudocódigo. Obviamente, la primera línea es innecesaria:
a = 1 a = 2 b = a
Pero esto es solo para nosotros. Para enseñarle al compilador a determinar esto, uno tendría que escribir algoritmos no triviales. Pero con SSA, esto es mucho más fácil. Ahora, incluso para un compilador simple, será obvio que el valor de la variable a1 no se usa, y esta línea se puede eliminar:
a1 = 1 a2 = 2 b1 = a2
SIL le permite aplicar optimizaciones y comprobaciones específicas al código Swift que serían difíciles o imposibles de completar en la fase AST.
Usando el generador SIL
Para generar SIL, use la bandera -emit-silgen :
swiftc -emit-silgen main.swift
El resultado del comando:
sil_stage raw import Builtin import Swift import SwiftShims let x: Int // x sil_global hidden [let] @$S4main1xSivp : $Int // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$S4main1xSivp // id: %2 %3 = global_addr @$S4main1xSivp : $*Int // user: %8 %4 = metatype $@thin Int.Type // user: %7 %5 = integer_literal $Builtin.Int2048, 16 // user: %7 // function_ref Int.init(_builtinIntegerLiteral:) %6 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %7 %7 = apply %6(%5, %4) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %8 store %7 to [trivial] %3 : $*Int // id: %8 %9 = integer_literal $Builtin.Int32, 0 // user: %10 %10 = struct $Int32 (%9 : $Builtin.Int32) // user: %11 return %10 : $Int32 // id: %11 } // end sil function 'main' // Int.init(_builtinIntegerLiteral:) sil [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int
SIL, como LLVM IR, se puede generar como código fuente. Puede encontrar en él que en esta etapa se agregó la importación de los módulos Swift Builtin, Swift y SwiftShims.
A pesar de que el código Swift se puede escribir directamente en el ámbito global, SILGen genera la función principal: el punto de entrada al programa. Todo el código estaba ubicado dentro de él, excepto para declarar una constante, ya que es global y debería estar accesible en todas partes.
La mayoría de las líneas tienen una estructura similar. A la izquierda hay un pseudo registro en el que se guarda el resultado de la instrucción. A continuación, la instrucción en sí y sus parámetros, y al final, un comentario que indica el registro para el que se utilizará este registro.
Por ejemplo, en esta línea se crea un literal entero de tipo Int2048 y un valor de 16. Este literal se almacena en el quinto registro y se usará para calcular el valor del séptimo:
%5 = integer_literal $Builtin.Int2048, 16 // user: %7
Una declaración de función comienza con la palabra clave sil. El siguiente es el nombre con el prefijo @, convención de llamada, parámetros, tipo de retorno y código de función. Para el inicializador Int.init (_builtinIntegerLiteral :), por supuesto, no se especifica, ya que esta función es de otro módulo, y solo necesita ser declarada, pero no definida. Un signo de dólar indica el comienzo de una indicación de tipo:
// Int.init(_builtinIntegerLiteral:) sil [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int
La convención de llamada indica cómo llamar correctamente a una función. Esto es necesario para generar código de máquina. Una descripción detallada de estos principios está más allá del alcance de este artículo.
El nombre de los inicializadores, así como los nombres de las estructuras, clases, métodos, protocolos, están distorsionados (cambio de nombre). Esto resuelve varios problemas a la vez.
En primer lugar, permite usar los mismos nombres en diferentes módulos y entidades anidadas. Por ejemplo, para el primer método fff , se usa el nombre S4main3AAAV3fffSiyF , y para el segundo, se usa S4main3BBBVVffffSiyF :
struct AAA { func fff() -> Int { return 8 } } struct BBB { func fff() -> Int { return 8 } }
S significa Swift, 4 es el número de caracteres en el nombre del módulo y 3 es el nombre de la clase. En el inicializador literal, Si denota el tipo estándar Swift.Int.
En segundo lugar, los nombres y tipos de argumentos de funciones se agregan al nombre. Esto permite el uso de sobrecarga. Por ejemplo, para el primer método, se genera S4main3AAAV3fff3iiiS2i_tF , y para el segundo - S4main3AAAV3fff3dddSiSd_tF :
struct AAA { func fff(iii internalName: Int) -> Int { return 8 } func fff(ddd internalName: Double) -> Int { return 8 } }
Después de los nombres de los parámetros, se indica el tipo del valor de retorno, seguido de los tipos de parámetros. Sin embargo, sus nombres internos no están indicados. Desafortunadamente, no hay documentación sobre el cambio de nombre en Swift, y su implementación puede cambiar en cualquier momento.
El nombre de la función es seguido por su definición. Consiste en uno o más bloques básicos. Un bloque básico es una secuencia de instrucciones con un punto de entrada, un punto de salida, que no contiene instrucciones de rama ni condiciones para una salida anticipada.
La función principal tiene una unidad base, que toma todos los parámetros pasados a la función como entrada y contiene todo su código, ya que no tiene ramas:
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
Podemos suponer que cada ámbito limitado por llaves es una unidad base separada. Supongamos que el código contiene una rama:
// before if 2 > 5 { // true } else { // false } // after
En este caso, se generarán al menos 4 bloques base para:
- código antes de ramificar,
- casos cuando la expresión es verdadera
- casos cuando la expresión es falsa
- código después de ramificar.
cond_br - instrucción para salto condicional. Si el valor del pseudo-registro% 14 es verdadero, entonces se realiza la transición al bloque bb1 . Si no, entonces en bb2 . br: salto incondicional que inicia la ejecución del bloque base especificado:
// before cond_br %14, bb1, bb2 // id: %15 bb1: // true br bb3 // id: %21 bb2: // Preds: bb0 // false br bb3 // id: %27 bb3: // Preds: bb2 bb1 // after
Código fuente:
La representación intermedia en bruto que se obtuvo en la última etapa se analiza para su corrección y se transforma en canónica: las funciones marcadas como transparentes están en línea (la llamada a la función se reemplaza por su cuerpo), se calculan los valores de las expresiones constantes, se verifica que las funciones que devuelven los valores haga esto en todas las ramas de código, etc.
Estas conversiones son obligatorias y se realizan incluso si la optimización del código está deshabilitada.
Canon SIL Generation
Para generar SIL canónico, se usa la bandera -emit-sil :
swiftc -emit-sil main.swift
El resultado del comando:
sil_stage canonical import Builtin import Swift import SwiftShims let x: Int // x sil_global hidden [let] @$S4main1xSivp : $Int // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$S4main1xSivp // id: %2 %3 = global_addr @$S4main1xSivp : $*Int // user: %6 %4 = integer_literal $Builtin.Int64, 16 // user: %5 %5 = struct $Int (%4 : $Builtin.Int64) // user: %6 store %5 to %3 : $*Int // id: %6 %7 = integer_literal $Builtin.Int32, 0 // user: %8 %8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9 return %8 : $Int32 // id: %9 } // end sil function 'main' // Int.init(_builtinIntegerLiteral:) sil public_external [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int { // %0 // user: %2 bb0(%0 : $Builtin.Int2048, %1 : $@thin Int.Type): %2 = builtin "s_to_s_checked_trunc_Int2048_Int64"(%0 : $Builtin.Int2048) : $(Builtin.Int64, Builtin.Int1) // user: %3 %3 = tuple_extract %2 : $(Builtin.Int64, Builtin.Int1), 0 // user: %4 %4 = struct $Int (%3 : $Builtin.Int64) // user: %5 return %4 : $Int // id: %5 } // end sil function '$SSi22_builtinIntegerLiteralSiBi2048__tcfC'
Hay pocos cambios en un ejemplo tan simple. Para ver el trabajo real del optimizador, debe complicar un poco el código. Por ejemplo, agregue la suma:
let x = 16 + 8
En su SIL sin procesar, puede encontrar la adición de estos literales:
%13 = function_ref @$SSi1poiyS2i_SitFZ : $@convention(method) (Int, Int, @thin Int.Type) -> Int // user: %14 %14 = apply %13(%8, %12, %4) : $@convention(method) (Int, Int, @thin Int.Type) -> Int // user: %15
Pero en lo canónico ya no está allí. En cambio, se usa un valor constante de 24:
%4 = integer_literal $Builtin.Int64, 24 // user: %5
Código fuente:
Optimización de sil
Se aplican transformaciones adicionales específicas de Swift si la optimización está habilitada. Entre ellos se encuentran la especialización de genéricos (optimización del código genérico para un tipo particular de parámetro), desvirtualización (reemplazo de llamadas dinámicas por estáticas), inlining, optimización ARC y mucho más. Una explicación de estas técnicas no encaja en un artículo ya cubierto.
Código fuente:
Como SIL es una característica de Swift, esta vez no mostré ejemplos de implementación. Volveremos al compilador de paréntesis en la siguiente parte cuando nos involucraremos en la generación de LLVM IR.