
Wir studieren weiterhin den Swift-Compiler. Dieser Teil ist der Swift Intermediate Language gewidmet.
Wenn Sie die vorherigen nicht gesehen haben, empfehle ich Ihnen, dem Link zu folgen und zu lesen:
Silgen
Der nächste Schritt besteht darin, den typisierten AST in rohen SIL zu konvertieren. Swift Intermediate Language (SIL) ist eine speziell für Swift erstellte Zwischendarstellung. Eine Beschreibung aller Anweisungen finden Sie in der Dokumentation .
SIL hat ein SSA-Formular. Static Single Assignment (SSA) - Eine Codedarstellung, bei der jeder Variablen nur einmal ein Wert zugewiesen wird. Es wird aus regulärem Code durch Hinzufügen zusätzlicher Variablen erstellt. Verwenden Sie beispielsweise ein numerisches Suffix, das die Version einer Variablen nach jeder Zuweisung angibt.
Dank dieses Formulars ist es für den Compiler einfacher, den Code zu optimieren. Unten finden Sie ein Beispiel für Pseudocode. Offensichtlich ist die erste Zeile unnötig:
a = 1 a = 2 b = a
Das ist aber nur für uns. Um dem Compiler beizubringen, dies zu bestimmen, müsste man nicht triviale Algorithmen schreiben. Mit SSA ist dies jedoch viel einfacher. Selbst für einen einfachen Compiler ist es offensichtlich, dass der Wert der Variablen a1 nicht verwendet wird, und diese Zeile kann gelöscht werden:
a1 = 1 a2 = 2 b1 = a2
Mit SIL können Sie bestimmte Optimierungen und Überprüfungen auf Swift-Code anwenden, die in der AST-Phase nur schwer oder gar nicht abgeschlossen werden können.
Verwenden des SIL-Generators
Verwenden Sie zum Generieren von SIL das Flag -emit-silgen :
swiftc -emit-silgen main.swift
Das Ergebnis des Befehls:
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 kann wie LLVM IR als Quellcode ausgegeben werden. Sie können darin feststellen, dass zu diesem Zeitpunkt der Import der Swift-Module Builtin, Swift und SwiftShims hinzugefügt wurde.
Trotz der Tatsache, dass Sie in Swift Code direkt im globalen Bereich schreiben können, generiert SILGen die Hauptfunktion, den Einstiegspunkt in das Programm. Der gesamte Code befand sich darin, mit Ausnahme der Deklaration einer Konstante, da er global ist und überall zugänglich sein sollte.
Die meisten Linien haben eine ähnliche Struktur. Links befindet sich ein Pseudoregister, in dem das Ergebnis der Anweisung gespeichert ist. Als nächstes - die Anweisung selbst und ihre Parameter und am Ende - ein Kommentar, der das Register angibt, für das dieses Register verwendet wird.
In dieser Zeile wird beispielsweise ein ganzzahliges Literal vom Typ Int2048 und ein Wert von 16 erstellt. Dieses Literal wird im fünften Register gespeichert und zur Berechnung des Werts des siebten Registers verwendet:
%5 = integer_literal $Builtin.Int2048, 16 // user: %7
Eine Funktionsdeklaration beginnt mit dem Schlüsselwort sil. Das Folgende ist der Name mit dem Präfix @, der Aufrufkonvention, den Parametern, dem Rückgabetyp und dem Funktionscode. Für den Int.init- Initialisierer (_builtinIntegerLiteral :) ist er natürlich nicht angegeben, da diese Funktion von einem anderen Modul stammt und nur deklariert, aber nicht definiert werden muss. Ein Dollarzeichen zeigt den Beginn einer Typangabe an:
// Int.init(_builtinIntegerLiteral:) sil [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int
Die Aufrufkonvention gibt an, wie eine Funktion ordnungsgemäß aufgerufen wird. Dies ist erforderlich, um Maschinencode zu generieren. Eine detaillierte Beschreibung dieser Prinzipien würde den Rahmen dieses Artikels sprengen.
Der Name der Initialisierer sowie die Namen der Strukturen, Klassen, Methoden und Protokolle sind verzerrt (Name Mangling). Dies löst mehrere Probleme gleichzeitig.
Erstens können dieselben Namen in verschiedenen Modulen und verschachtelten Entitäten verwendet werden. Beispielsweise wird für die erste Methode fff der Name S4main3AAAV3fffSiyF verwendet , und für die zweite Methode wird S4main3BBBVVffffSiyF verwendet :
struct AAA { func fff() -> Int { return 8 } } struct BBB { func fff() -> Int { return 8 } }
S bedeutet Swift, 4 ist die Anzahl der Zeichen im Modulnamen und 3 ist im Klassennamen. Im Literalinitialisierer bezeichnet Si den Standardtyp Swift.Int.
Zweitens werden dem Namen Namen und Arten von Funktionsargumenten hinzugefügt. Dies ermöglicht die Verwendung von Überlast. Für die erste Methode wird beispielsweise S4main3AAAV3fff3iiiS2i_tF generiert, und für die zweite Methode - S4main3AAAV3fff3dddSiSd_tF :
struct AAA { func fff(iii internalName: Int) -> Int { return 8 } func fff(ddd internalName: Double) -> Int { return 8 } }
Nach den Parameternamen wird der Typ des Rückgabewerts angegeben, gefolgt von den Parametertypen. Ihre internen Namen werden jedoch nicht angegeben. Leider gibt es in Swift keine Dokumentation zum Mangeln von Namen, und die Implementierung kann sich jederzeit ändern.
Dem Namen der Funktion folgt ihre Definition. Es besteht aus einem oder mehreren Grundblöcken. Ein Basisblock ist eine Folge von Anweisungen mit einem Eintrittspunkt und einem Austrittspunkt, die keine Verzweigungsbefehle oder Bedingungen für einen vorzeitigen Austritt enthalten.
Die Hauptfunktion hat eine Basiseinheit, die alle an die Funktion übergebenen Parameter als Eingabe verwendet und ihren gesamten Code enthält, da keine Verzweigungen darin sind:
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
Wir können davon ausgehen, dass jeder durch geschweifte Klammern begrenzte Bereich eine separate Basiseinheit ist. Angenommen, der Code enthält einen Zweig:
// before if 2 > 5 { // true } else { // false } // after
In diesem Fall werden mindestens 4 Basisblöcke generiert für:
- Code vor der Verzweigung,
- Fälle, in denen der Ausdruck wahr ist
- Fälle, in denen der Ausdruck falsch ist
- Code nach der Verzweigung.
cond_br - Anweisung für den bedingten Sprung. Wenn der Pseudoregister% 14-Wert wahr ist, wird der Übergang zum Block bb1 durchgeführt . Wenn nicht, dann in bb2 . br - bedingungsloser Sprung, der die Ausführung des angegebenen Basisblocks startet:
// 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
Quellcode:
Die rohe Zwischendarstellung, die in der letzten Stufe erhalten wurde, wird auf Richtigkeit analysiert und in kanonisch umgewandelt: Die als transparent gekennzeichneten Funktionen sind inline (der Funktionsaufruf wird durch seinen Körper ersetzt), die Werte konstanter Ausdrücke werden berechnet, die Funktion wird überprüft, ob die Funktionen die Werte zurückgeben Tun Sie dies in allen Code-Zweigen und so weiter.
Diese Konvertierungen sind obligatorisch und werden auch dann durchgeführt, wenn die Codeoptimierung deaktiviert ist.
Canon SIL Generation
Um kanonisches SIL zu generieren, wird das Flag -emit-sil verwendet:
swiftc -emit-sil main.swift
Das Ergebnis des Befehls:
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'
In einem so einfachen Beispiel gibt es nur wenige Änderungen. Um die eigentliche Arbeit des Optimierers zu sehen, müssen Sie den Code etwas komplizieren. Fügen Sie beispielsweise einen Zusatz hinzu:
let x = 16 + 8
In seinem rohen SIL finden Sie die Hinzufügung dieser Literale:
%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
Aber im Kanonischen ist es nicht mehr da. Stattdessen wird ein konstanter Wert von 24 verwendet:
%4 = integer_literal $Builtin.Int64, 24 // user: %5
Quellcode:
Sil Optimierung
Zusätzliche Swift-spezifische Transformationen werden angewendet, wenn die Optimierung aktiviert ist. Dazu gehören die Spezialisierung von Generika (Optimierung des generischen Codes für einen bestimmten Parametertyp), die Devirtualisierung (Ersetzen dynamischer Aufrufe durch statische), Inlining, ARC-Optimierung und vieles mehr. Eine Erklärung dieser Techniken passt nicht in einen bereits überwucherten Artikel.
Quellcode:
Da SIL eine Swift-Funktion ist, habe ich diesmal keine Implementierungsbeispiele gezeigt. Wir werden im nächsten Teil zum Klammer-Compiler zurückkehren, wenn wir uns mit der LLVM-IR-Generierung beschäftigen.