var vs let in SIL

Hallo zusammen, ich bin @kitasuke, iOS Engineer.

Dies ist mein erster Beitrag, "var vs let in SIL" als Serie von "Swift Type in SIL". Heute werde ich Ihnen mitteilen, was ich über die Funktionsweise von var und let in SIL gelernt habe.

Wenn Sie an anderen Posts interessiert sind, finden Sie hier weitere Details.

var und lass

Wir benutzen var und lassen täglich viel, aber lassen Sie mich kurz darauf eingehen.

var

Da var als Variable bekannt ist, kann ein Wert von var mehrmals festgelegt oder geändert werden.

var x: Int
x = 1
x = 10

Lassen

Andererseits kann, wie let als Konstante bezeichnet wird, ein Wert von let nicht mehr geändert werden, wenn er einmal festgelegt wurde.

let x: Int
x = 1
x = 10 // Fehler

Es ist also ganz einfach. Die Differenz ist buchstäblich variabel oder konstant.

SIL

Schauen wir uns als nächstes an, wie sie in SIL dargestellt werden.

Beispiele

Es gibt eine einfache Funktionsnummer (), die den Int-Wert zurückgibt. Der einzige Unterschied zwischen zwei Dateien besteht darin, ob der Int-Wert als var oder let deklariert ist.

var.swift

Funknummer () -> Int {
    var x: Int
    x = 1
    return x
}

let.swift

Funknummer () -> Int {
    let x: Int
    x = 1
    return x
}

rohes SIL

Lassen Sie uns mit dem folgenden Befehl swiftc RAW-SIL für var.swift generieren.

$ swiftc -emit-silgen var.swift -o var.silgen

Unten ist var.silgen, das rohe SIL von var.swift ist. Möglicherweise werden Ihnen unbekannte Funktionen angezeigt, die Sie jedoch noch nicht verstehen müssen.

var.silgen

% 0 = alloc_box $ {var Int}, var, name "x"
% 1 = mark_uninitialized [var]% 0: $ {var Int}
% 2 = project_box% 1: $ {var Int}, 0
% 3 = Metatyp $ @ thin Int.Type
% 4 = integer_literal $ Builtin.Int2048, 1
// function_ref Int.init (_builtinIntegerLiteral :)
% 5 = function_ref @ $ SSi22_builtinIntegerLiteralSiBi2048__tcfC:
    $ @ convention (Methode) (Builtin.Int2048, @thin Int.Type) -> Int
% 6 =% 5 anwenden (% 4,% 3): $ @ Konvention (Methode) (Builtin.Int2048, @thin Int.Type) -> Int
% 7 = begin_access [ändern] [unbekannt]% 2: $ * Int
% 6 bis% 7 zuweisen: $ * Int
end_access% 7: $ * Int
% 10 = begin_access [read] [unknown]% 2: $ * Int
% 11 = Laden [trivial]% 10: $ * Int
end_access% 10: $ * Int
destroy_value% 1: $ {var Int}
return% 11: $ Int

Gleicher Befehl swiftc für let.swift.

$ swiftc -emit-silgen let.swift -o let.silgen

Unten ist let.silgen, das rohe SIL von let.swift ist.

let.silgen

% 0 = alloc_stack $ Int, let, name "x"
% 1 = mark_uninitialized [var]% 0: $ * Int
% 2 = Metatyp $ @ thin Int.Type
% 3 = integer_literal $ Builtin.Int2048, 1
// function_ref Int.init (_builtinIntegerLiteral :)
% 4 = function_ref @ $ SSi22_builtinIntegerLiteralSiBi2048__tcfC:
    $ @ convention (Methode) (Builtin.Int2048, @thin Int.Type) -> Int
% 5 =% 4 anwenden (% 3,% 2): $ @ Konvention (Methode) (Builtin.Int2048, @thin Int.Type) -> Int
% 5% 1 zuweisen: $ * Int
% 7 = Laden [trivial]% 1: $ * Int
dealloc_stack% 0: $ * Int
return% 7: $ Int

Diff

Ich werde die Unterschiede hervorheben, damit Sie sie leicht erkennen können.

Wenn Sie sich das Diff genau ansehen, sehen Sie alloc_box und begin_access in var.silgen.

% 0 = alloc_box $ {var Int}, var, name "x"
% 1 = mark_uninitialized [var]% 0: $ {var Int}
% 2 = project_box% 1: $ {var Int}, 0
% 7 = begin_access [ändern] [unbekannt]% 2: $ * Int
end_access% 7: $ * Int
% 10 = begin_access [read] [unknown]% 2: $ * Int
end_access% 10: $ * Int
destroy_value% 1: $ {var Int}

In let.silgen wird jedoch alloc_stack und nicht alloc_box angezeigt.

% 0 = alloc_stack $ Int, let, name "x"
% 1 = mark_uninitialized [var]% 0: $ * Int
dealloc_stack% 0: $ * Int

Was ist der Unterschied zwischen alloc_box und alloc_stack? Ich denke, dies kann uns zu einem tiefen Verständnis dessen führen, was var und let sind.

alloc_box vs alloc_stack

alloc_box

Ordnet eine @box mit Referenzzählung auf dem Heap zu, die groß genug ist, um einen Wert vom Typ T zusammen mit einer Beibehaltungszählung und allen anderen für die Laufzeit erforderlichen Metadaten aufzunehmen. Das Ergebnis der Anweisung ist die referenzzählende @box-Referenz, der die Box gehört. Die Anweisung project_box wird verwendet, um die Adresse des Werts in der Box abzurufen.
 Die Box wird mit einer Anzahl von 1 initialisiert. Der Speicher wird nicht initialisiert. Die Box besitzt den enthaltenen Wert, und wenn Sie sie auf Null setzen, wird der enthaltene Wert wie durch destroy_addr zerstört. Das Freigeben einer Box ist ein undefiniertes Verhalten, wenn der Wert der Box nicht initialisiert ist. Zum Aufheben der Zuordnung einer Box, deren Wert nicht initialisiert wurde, sollte dealloc_box verwendet werden.

Gemäß der Dokumentation weist alloc_box einen Referenzzählwert auf dem Heap zu. Es muss manuell verwaltet werden, indem die Anzahl beibehalten wird.

alloc_stack

Weist nicht initialisierten Speicher zu, der auf dem Stapel so ausgerichtet ist, dass er einen Wert vom Typ T enthält. Das Ergebnis des Befehls ist die Adresse des zugewiesenen Speichers. Wenn ein Typ eine Laufzeitgröße hat, muss der Compiler Code ausgeben, um möglicherweise Speicher dynamisch zuzuweisen. Es gibt also keine Garantie dafür, dass sich der zugewiesene Speicher wirklich auf dem Stapel befindet. alloc_stack markiert den Beginn der Lebensdauer des Werts. Die Zuordnung muss mit einem Befehl dealloc_stack abgeglichen werden, um das Ende seiner Lebensdauer zu markieren. Alle alloc_stack-Zuordnungen müssen vor der Rückkehr von einer Funktion freigegeben werden. Wenn ein Block mehrere Vorgänger enthält, müssen Stapelhöhe und Zuordnungsreihenfolge für alle Vorgängerblöcke konsistent sein. alloc_stack-Zuweisungen müssen in der Reihenfolge des letzten Eingangs und des ersten Ausgangs des Stapels freigegeben werden.
Der Speicher kann nicht beibehalten werden. Verwenden Sie alloc_box, um ein Aufbewahrungsfach für einen Wertetyp zuzuweisen.

Alloc_stack weist laut Dokumentation einen Wert auf dem Stack zu. Es ist keine Referenzzählung. Alle alloc_stack-Zuordnungen müssen vor der Rückkehr von einer Funktion freigegeben werden.

Der große Unterschied wäre hier eine lebenslange Wertschöpfung. Wenn Sie beispielsweise eine Variable außerhalb des Closures deklariert haben, diese aber im Closure verwendet wird, wird ihr Wert möglicherweise geändert. In diesem Fall sollte die Zählung von alloc_box beibehalten werden. Wenn Sie jedoch eine Variable innerhalb der Funktion deklariert haben, sollte die Zuordnung durch alloc_stack aufgehoben werden.

Ich dachte, dass var es uns ermöglicht, seinen Wert nur mehrfach zu ändern, aber es kann auch außerhalb des Gültigkeitsbereichs durchgeführt werden. Deshalb wird alloc_box für die Referenzzählung verwendet.

Denken Sie mal darüber nach. In unserem Beispiel haben wir nur eine lokale var inside-Funktion verwendet und sie wurde nie außerhalb des Gültigkeitsbereichs geändert. Muss die alloc_box wirklich für den Fall verwendet werden? Schauen wir uns als nächstes das kanonische SIL an.

kanonisches SIL

Hier ist der Befehl swiftc zum Ausgeben von kanonischem SIL.

$ swiftc -emit-sil var.swift -o var.sil

Unten ist var.sil, das kanonisches SIL von var.swift ist.

var.sil

% 0 = alloc_stack $ Int, var, name "x"
% 1 = integer_literal $ Builtin.Int64, 1
% 2 = struct $ Int (% 1: $ Builtin.Int64)
% 3 = begin_access [ändern] [statisch]% 0: $ * Int
% 2 bis% 3 speichern: $ * Int
end_access% 3: $ * Int
% 6 = begin_access [read] [static]% 0: $ * Int
end_access% 6: $ * Int
dealloc_stack% 0: $ * Int
return% 2: $ Int

Es ist ein bisschen anders als var.silgen. Wie erwartet wird alloc_box in var.sil durch alloc_stack ersetzt. Wie ist es passiert? Dies ist Teil der Optimierungen in swiftc. Genauer gesagt handelt es sich im AllocBoxToStack-Modul um "Box-to-Stack-Promotion". Die Idee ist, dass swiftc unnötige Heap-Zuweisungen zum Stapeln fördert. Weitere Details entnehmen Sie bitte dem unten stehenden Link.

Gleicher Befehl swiftc für let.swift.

$ swiftc -emit-sil let.swift -o let.sil

Unten ist let.sil, das kanonisches SIL von let.swift.

let.sil

% 0 = alloc_stack $ Int, let, name "x"
% 1 = integer_literal $ Builtin.Int64, 1
% 2 = struct $ Int (% 1: $ Builtin.Int64)
% 2 bis% 0 speichern: $ * Int
dealloc_stack% 0: $ * Int
return% 2: $ Int

Hier gibt es keine wesentlichen Unterschiede. Raw SIL war einfach genug, daher gibt es in diesem Durchgang wohl nichts zu optimieren.

Zusammenfassung

Heute sind wir in var eingetaucht und haben SIL eingelassen. Wir haben herausgefunden, dass es für Swift-Werte eine Lebensdauer gibt. Auch ein schneller Compiler ist wirklich schlau. Es hat viele Optimierungen, nicht nur die, die ich in diesem Beitrag erklärt habe. Ich dachte, dass var und let nur einfach sind, aber sie werden hinter den Kulissen des Compilers gut berücksichtigt. Es mag zu detailliertes Wissen sein, aber es ist immer gut zu wissen, wie es als Swift-Entwickler funktioniert.

Verweise

swift / docs / SIL.rst

Swifts High-Level-IR: Eine Fallstudie zur Ergänzung von LLVM-IR durch sprachspezifische Optimierung