Dispositif de compilateur Swift. 3e partie


Nous continuons à étudier le compilateur Swift. Cette partie est dédiée au Swift Intermediate Language.


Si vous n'avez pas vu les précédents, je vous recommande de suivre le lien et de lire:



Silgen


L'étape suivante consiste à convertir l'AST typé en SIL brut. Swift Intermediate Language (SIL) est une représentation intermédiaire spécialement créée pour Swift. Une description de toutes les instructions se trouve dans la documentation .


SIL a une forme SSA. Static Single Assignment (SSA) - Représentation de code dans laquelle chaque variable se voit attribuer une valeur une seule fois. Il est créé à partir de code normal en ajoutant des variables supplémentaires. Par exemple, utiliser un suffixe numérique qui indique la version d'une variable après chaque affectation.


Grâce à ce formulaire, il est plus facile pour le compilateur d'optimiser le code. Ci-dessous, un exemple de pseudo-code. De toute évidence, la première ligne est inutile:


a = 1 a = 2 b = a 

Mais ce n'est que pour nous. Pour apprendre au compilateur à déterminer cela, il faudrait écrire des algorithmes non triviaux. Mais avec SSA, c'est beaucoup plus facile. Maintenant, même pour un simple compilateur, il sera évident que la valeur de la variable a1 n'est pas utilisée, et cette ligne peut être supprimée:


 a1 = 1 a2 = 2 b1 = a2 

SIL vous permet d'appliquer des optimisations et des contrôles spécifiques au code Swift qui seraient difficiles, voire impossibles à réaliser dans la phase AST.


Utilisation de SIL Generator


Pour générer SIL, utilisez l' indicateur -emit-silgen :


 swiftc -emit-silgen main.swift 

Le résultat de la commande:


 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, comme LLVM IR, peut être sorti en tant que code source. Vous pouvez y trouver qu'à ce stade, l'importation des modules Swift Builtin, Swift et SwiftShims a été ajoutée.


Malgré le fait que le code Swift peut être écrit directement dans la portée globale, SILGen génère la fonction principale - le point d'entrée du programme. Tout le code était situé à l'intérieur, sauf pour déclarer une constante, car il est global et devrait être accessible partout.


La plupart des lignes ont une structure similaire. À gauche, un pseudo-registre dans lequel le résultat de l'instruction est enregistré. Ensuite - l'instruction elle-même et ses paramètres, et à la fin - un commentaire indiquant le registre pour lequel ce registre sera utilisé.


Par exemple, un littéral entier de type Int2048 et une valeur de 16. est créé sur cette ligne. Ce littéral est stocké dans le cinquième registre et sera utilisé pour calculer la valeur du septième:


 %5 = integer_literal $Builtin.Int2048, 16 // user: %7 

Une déclaration de fonction commence par le mot-clé sil. Voici le nom avec le préfixe @, la convention d'appel, les paramètres, le type de retour et le code de fonction. Pour l'initialiseur Int.init (_builtinIntegerLiteral :), il n'est bien sûr pas spécifié, car cette fonction provient d'un autre module et n'a besoin que d'être déclarée, mais non définie. Un signe dollar indique le début d'une indication de type:


 // Int.init(_builtinIntegerLiteral:) sil [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int 

La convention d'appel indique comment appeler correctement une fonction. Cela est nécessaire pour générer le code machine. Une description détaillée de ces principes dépasse le cadre de cet article.


Le nom des initialiseurs, ainsi que les noms des structures, classes, méthodes, protocoles, sont déformés (nom mangling). Cela résout plusieurs problèmes à la fois.


Tout d'abord, il permet d'utiliser les mêmes noms dans différents modules et entités imbriquées. Par exemple, pour la première méthode fff , le nom S4main3AAAV3fffSiyF est utilisé , et pour la seconde, S4main3BBBVVffffSiyF est utilisé :


 struct AAA { func fff() -> Int { return 8 } } struct BBB { func fff() -> Int { return 8 } } 

S signifie Swift, 4 est le nombre de caractères dans le nom du module et 3 dans le nom de la classe. Dans l'initialiseur littéral, Si désigne le type standard Swift.Int.


Deuxièmement, les noms et types d'arguments de fonction sont ajoutés au nom. Cela permet d'utiliser la surcharge. Par exemple, pour la première méthode, S4main3AAAV3fff3iiiS2i_tF est généré, et pour la seconde - S4main3AAAV3fff3dddSiSd_tF :


 struct AAA { func fff(iii internalName: Int) -> Int { return 8 } func fff(ddd internalName: Double) -> Int { return 8 } } 

Après les noms des paramètres, le type de la valeur de retour est indiqué, suivi des types de paramètres. Cependant, leurs noms internes ne sont pas indiqués. Malheureusement, il n'y a pas de documentation sur le changement de nom dans Swift, et son implémentation peut changer à tout moment.


Le nom de la fonction est suivi de sa définition. Il se compose d'un ou plusieurs blocs de base. Un bloc de base est une séquence d'instructions avec un point d'entrée, un point de sortie, qui ne contient pas d'instructions de branchement ni de conditions pour une sortie anticipée.


La fonction principale a une unité de base, qui prend tous les paramètres passés à la fonction en entrée et contient tout son code, car il n'y a pas de branches:


 bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): 

Nous pouvons supposer que chaque étendue délimitée par des accolades est une unité de base distincte. Supposons que le code contienne une branche:


 // before if 2 > 5 { // true } else { // false } // after 

Dans ce cas, au moins 4 blocs de base seront générés pour:


  • code avant de se ramifier,
  • cas où l'expression est vraie
  • cas où l'expression est fausse
  • code après branchement.

cond_br - instruction pour le saut conditionnel. Si la valeur du pseudo-registre% 14 est vraie, alors la transition vers le bloc bb1 est effectuée . Sinon, alors dans bb2 . br - saut inconditionnel qui démarre l'exécution du bloc de base spécifié:


 // 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 

Code source:



Transformations garanties SIL


La représentation intermédiaire brute obtenue à la dernière étape est analysée pour être correcte et transformée en canonique: les fonctions marquées transparentes sont en ligne (l'appel de fonction est remplacé par son corps), les valeurs des expressions constantes sont calculées, la fonction est vérifiée que les fonctions qui renvoient les valeurs faites cela dans toutes les branches de code et ainsi de suite.


Ces conversions sont obligatoires et sont effectuées même si l'optimisation de code est désactivée.


Canon SIL Generation


Pour générer un SIL canonique, le drapeau -emit-sil est utilisé:


 swiftc -emit-sil main.swift 

Le résultat de la commande:


 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' 

Il y a peu de changements dans un exemple aussi simple. Pour voir le vrai travail de l'optimiseur, vous devez compliquer un peu le code. Par exemple, ajoutez un ajout:


 let x = 16 + 8 

Dans son SIL brut, vous pouvez trouver l'ajout de ces littéraux:


 %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 

Mais dans le canonique, il n'est plus là. Au lieu de cela, une valeur constante de 24 est utilisée:


 %4 = integer_literal $Builtin.Int64, 24 // user: %5 

Code source:



Optimisation du sil


Des transformations spécifiques à Swift supplémentaires sont appliquées si l'optimisation est activée. Parmi eux, la spécialisation des génériques (optimisation du code générique pour un type de paramètre particulier), la dévirtualisation (remplacement des appels dynamiques par des appels statiques), l'inline, l' optimisation ARC, et bien plus encore. Une explication de ces techniques ne rentre pas dans un article déjà envahi.


Code source:



Étant donné que SIL est une fonctionnalité Swift, je n'ai pas montré d'exemples d'implémentation cette fois. Nous reviendrons sur le compilateur de parenthèses dans la prochaine partie lorsque nous serons engagés dans la génération IR LLVM.

Source: https://habr.com/ru/post/fr438696/


All Articles