var vs nechat v SIL

Ahoj všem, jsem @ kitasuke, iOS Engineer.

Toto je můj první příspěvek „var vs let in SIL“ jako série „Swift type in SIL“. Dnes se chystám podělit o to, co jsem se dozvěděl o tom, jak var a nechal pracovat v SIL.

Pokud vás zajímají další příspěvky, další podrobnosti naleznete zde.

var a nechat

Používáme var a necháváme hodně denně, ale dovolte mi o nich stručně vysvětlit.

var

Protože var je znám jako proměnná, lze hodnotu var nastavit vícekrát nebo vícekrát.

var x: Int
x = 1
x = 10

nechat

Na druhou stranu, jak je let známo jako konstanta, hodnotu let nelze po nastavení změnit.

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

Takže je to docela jednoduché. Rozdíl je doslova variabilní nebo konstantní.

SIL

Dále se podívejme, jak jsou zastoupeni v SIL.

Příklady

Existuje jednoduché číslo funkce (), které vrací hodnotu Int. Jediný rozdíl mezi dvěma soubory je v tom, zda je hodnota Int deklarována jako var nebo let.

var.swift

func number () -> Int {
    var x: Int
    x = 1
    návrat x
}

let.swift

func number () -> Int {
    let x: Int
    x = 1
    návrat x
}

surový SIL

Generujme surovou SIL pro var.swift s příkazem swiftc níže.

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

Níže je var.silgen, což je surový SIL var.swift. Možná uvidíte neznámé funkce, ale není třeba jim rozumět.

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:
    Konvence $ @ (metoda) (Builtin.Int2048, @ Thin Int.Type) -> Int
% 6 = použít% 5 (% 4,% 3): $ @ konvence (metoda) (Builtin.Int2048, @thin Int.Type) -> Int
% 7 = begin_access [změnit] [neznámý]% 2: $ * Int
přiřadit% 6 až% 7: $ * Int
end_access% 7: $ * Int
% 10 = begin_access [přečteno] [neznámé]% 2: $ * Int
% 11 = zatížení [triviální]% 10: $ * Int
end_access% 10: $ * Int
destroy_value% 1: $ {var Int}
návratnost% 11: $ Int

Stejný příkaz swiftc pro let.swift.

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

Níže je let.silgen, což je surová SIL let.swift.

let.silgen

% 0 = alloc_stack $ Int, nechť, jméno "x"
% 1 = mark_uninitialized [var]% 0: $ * Int
% 2 = metatyp $ @ thin Int.Type
% 3 = celé číslo $ Builtin.Int2048, 1
// function_ref Int.init (_builtinIntegerLiteral :)
% 4 = function_ref @ $ SSi22_builtinIntegerLiteralSiBi2048__tcfC:
    Konvence $ @ (metoda) (Builtin.Int2048, @ Thin Int.Type) -> Int
% 5 = použít% 4 (% 3,% 2): $ @ konvence (metoda) (Builtin.Int2048, @thin Int.Type) -> Int
přiřadit% 5 až% 1: $ * Int
% 7 = zatížení [triviální]% 1: $ * Int
dealloc_stack% 0: $ * Int
návratnost% 7: $ Int

Rozdíl

Zdůrazním rozdíly, abyste je mohli snadno vidět.

Pokud se podrobně podíváte na rozdíl, uvidíte varcilgen aloc_box a begin_access.

% 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 [změnit] [neznámý]% 2: $ * Int
end_access% 7: $ * Int
% 10 = begin_access [přečteno] [neznámé]% 2: $ * Int
end_access% 10: $ * Int
destroy_value% 1: $ {var Int}

Můžete však vidět alloc_stack, nikoli alloc_box v let.silgen.

% 0 = alloc_stack $ Int, nechť, jméno "x"
% 1 = mark_uninitialized [var]% 0: $ * Int
dealloc_stack% 0: $ * Int

Jaký je rozdíl mezi alloc_box a alloc_stack? Myslím, že to může vést k hlubokému pochopení toho, co var a let jsou.

alloc_box vs alloc_stack

alloc_box

Přiděluje @boxu s počítáním referencí na haldě dostatečně velký na to, aby udržel hodnotu typu T, spolu se zachováním počtu a všech dalších metadat požadovaných runtime. Výsledkem instrukce je referenční @ počítaný odkaz @box, který vlastní krabici. Instrukce project_box se používá k načtení adresy hodnoty uvnitř pole.
 Krabice bude inicializována s udržovacím počtem 1; úložiště bude neinicializované. Krabice vlastní obsaženou hodnotu a uvolněním ji do udržovacího počtu nula zničí obsaženou hodnotu, jako by to destroy_addr. Uvolnění pole je nedefinované chování, pokud je hodnota pole neinicializovaná. K uvolnění pole, jehož hodnota nebyla inicializována, by mělo být použito dealloc_box.

Podle dokumentace aloc_box přiděluje referenční počítanou hodnotu na haldu. Musí být ručně spravována pomocí počtu uchování.

alloc_stack

Přiděluje neinicializovanou paměť, která je dostatečně zarovnána v zásobníku, aby obsahovala hodnotu typu T. Výsledkem instrukce je adresa přidělené paměti. Pokud je typ typu runtime, kompilátor musí vyslat kód, aby potenciálně dynamicky alokoval paměť. Neexistuje tedy žádná záruka, že přidělená paměť je skutečně umístěna na zásobníku. alloc_stack označuje začátek životnosti hodnoty; přidělení musí být vyváženo instrukcí dealloc_stack, aby bylo možné označit konec jeho životnosti. Všechny přidělení alloc_stack musí být přiděleny před návratem z funkce. Pokud má blok více předchůdců, musí být výška zásobníku a pořadí přidělení shodné pocházející ze všech předchůdců. alokace přidělování alokací musí být přiděleny v pořadí zásobníků na první a první místo.
Paměť není uchovatelná. Chcete-li přidělit udržitelné pole pro typ hodnoty, použijte alloc_box.

Podle dokumentace přidělí aloc_stack hodnotu na zásobníku. Není to počítání referencí. Všechny přidělení alloc_stack musí být přiděleny před návratem z funkce.

Velkým rozdílem by byla celoživotní hodnota. Například pokud máte proměnnou deklarovanou mimo uzavření, ale používá se v uzávěrce, může být její hodnota upravena. V takovém případě by měla být ponechána počítána aloc_box. Pokud však máte uvnitř funkce deklarovanou proměnnou, měla by být přidělena aloc_stack.

Přemýšlel jsem, že var nám umožňuje jen několikrát změnit jeho hodnotu, ale lze to provést i mimo rozsah. Proto se alloc_box používá pro počítání referencí.

Přemýšlejte o tom, V našem příkladu jsme právě použili funkci lokální var uvnitř a její nikdy se nezměnili. Skutečně musí pro tento případ použít alloc_box? Podívejme se dále na kanonický SIL.

kanonický SIL

Zde je příkaz swiftc, který vydává kanonický SIL.

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

Níže je var.sil, což je kanonický SIL var.swift.

var.sil

% 0 = alloc_stack $ Int, var, name "x"
% 1 = celé číslo $ Builtin.Int64, 1
% 2 = struct $ Int (% 1: $ Builtin.Int64)
% 3 = begin_access [změnit] [statický]% 0: $ * Int
uložit% 2 až% 3: $ * Int
end_access% 3: $ * Int
% 6 = begin_access [přečteno] [statické]% 0: $ * Int
end_access% 6: $ * Int
dealloc_stack% 0: $ * Int
návrat% 2: $ Int

Je to trochu jiné než var.silgen. Jak se očekávalo, alloc_box je nahrazen aloc_stack v var.sil. Jak se to stane? Toto je část optimalizace v swiftc. Přesněji řečeno, je to „propagace boxu do zásobníku“ v modulu AllocBoxToStack. Myšlenka je, že swiftc podporuje zbytečné přidělení haldy do zásobníku. Další podrobnosti naleznete níže uvedený odkaz.

Stejný příkaz swiftc pro let.swift.

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

Níže je let.sil, což je kanonický SIL let.swift.

let.sil

% 0 = alloc_stack $ Int, nechť, jméno "x"
% 1 = celé číslo $ Builtin.Int64, 1
% 2 = struct $ Int (% 1: $ Builtin.Int64)
uložit% 2 do% 0: $ * Int
dealloc_stack% 0: $ * Int
návrat% 2: $ Int

Nejsou zde žádné významné rozdíly. Raw SIL byl dost jednoduchý, takže myslím, že v tomto průchodu není co optimalizovat.

souhrn

Dnes jsme se ponořili do varu a pustili SIL. Zjistili jsme, že pro hodnoty Swift existuje životnost. Také rychlý kompilátor je opravdu chytrý. Má spoustu optimalizací, nejen to, které jsem vysvětlil v tomto příspěvku. Myslel jsem, že var a let jsou prostě jednoduché, ale jsou dobře považovány za scény v kompilátoru. Může to být příliš podrobné znalosti, ale vždy je dobré vědět, jak to funguje jako vývojář Swift.

Reference

swift / docs / SIL.rst

Swift's High-Level IR: Případová studie doplnění LLVM IR s jazykovou optimalizací