
Продолжаем изучать компилятор Swift. Эта часть посвящена Swift Intermediate Language.
Если вы не видели предыдущие, рекомендую перейти по ссылке и прочитать:
SILGen
Следующий шаг — преобразование типизированного AST в сырой SIL. Swift Intermediate Language(SIL) — это специально созданное для Swift промежуточное представление. Описание всех инструкций можно найти в документации.
SIL имеет SSA форму. Static Single Assignment (SSA) — представление кода, в котором каждой переменной значение присваивается только один раз. Оно создаётся из обычного кода добавлением дополнительных переменных. Например, с помощью числового суффикса, который обозначает версию переменной после каждого присваивания.
Благодаря этой форме компилятору проще оптимизировать код. Ниже приведён пример на псевдокоде. Очевидно, что первая строка является ненужной:
a = 1
a = 2
b = a
Но это только для нас. Чтобы научить компилятор это определять, пришлось бы писать нетривиальные алгоритмы. Но с помощью SSA сделать это гораздо проще. Теперь даже для простого компилятора будет очевидно, что значение переменной a1 не используется, и эту строку можно удалить:
a1 = 1
a2 = 2
b1 = a2
SIL позволяет применять к коду Swift специфичные оптимизации и проверки, которые было бы сложно или невозможно осуществить на этапе AST.
Использование генератора SIL
Для генерации SIL используется флаг -emit-silgen:
swiftc -emit-silgen main.swift
Результат выполнения команды:
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, как и LLVM IR, можно вывести в виде исходного кода. В нём можно обнаружить, что на этом этапе был добавлен импорт Swift модулей Builtin, Swift и SwiftShims.
Не смотря на то, что в Swift код можно писать прямо в глобальной области видимости, SILGen генерирует функцию main — точку входа в программу. Весь код был расположен внутри неё, кроме объявления константы, так как она является глобальной и должна быть доступна везде.
Большая часть строк имеет похожую структуру. Слева расположен псевдорегистр, в который сохраняется результат выполнения инструкции. Далее — сама инструкция и её параметры, а в конце — комментарий с указанием регистра, для вычисления которого будет использоваться этот регистр.
Например, в этой строке создаётся целочисленный литерал с типом Int2048 и значением 16. Этот литерал сохраняется в пятый регистр и будет использован для вычисления значения седьмого:
%5 = integer_literal $Builtin.Int2048, 16 // user: %7
Объявление функции начинается с ключевого слова sil. Далее указывается название с префиксом @, calling convention, параметры, тип возвращаемого значения и код функции. Для инициализатора Int.init(_builtinIntegerLiteral:) он, естественно, не указан, так как эта функция из другого модуля, и её нужно только объявить, но не определять. Символ доллара означает начало указания типа:
// Int.init(_builtinIntegerLiteral:)
sil [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int
Calling convention указывает на то, как правильно вызывать функцию. Это необходимо для генерации машинного кода. Подробное описание этих принципов выходит за рамки статьи.
Название инициализаторов, как и имена структур, классов, методов, протоколов, искажаются (name mangling). Это решает сразу несколько проблем.
Во-первых, это позволяет использовать одинаковые имена в разных модулях и вложенных сущностях. Например, для первого метода fff используется имя S4main3AAAV3fffSiyF, а для второго — S4main3BBBV3fffSiyF:
struct AAA {
func fff() -> Int {
return 8
}
}
struct BBB {
func fff() -> Int {
return 8
}
}
S значит Swift, 4 — это число символов в названии модуля, а 3 — в названии класса. В инициализаторе литерала Si обозначает стандартный тип Swift.Int.
Во-вторых, в название добавляются имена и типы аргументов функций. Это позволяет использовать перегрузку. Например, для первого метода генерируется S4main3AAAV3fff3iiiS2i_tF, а для второго — S4main3AAAV3fff3dddSiSd_tF:
struct AAA {
func fff(iii internalName: Int) -> Int {
return 8
}
func fff(ddd internalName: Double) -> Int {
return 8
}
}
После названий параметров указан тип возвращаемого значения, а за ним — типы параметров. При этом их внутренние названия не указываются. К сожалению, документации по name mangling в Swift нет, а его реализация может в любой момент измениться.
За названием функции следует её определение. Оно состоит из одного или нескольких basic block. Базовый блок — последовательность инструкций с одной точкой входа, одной точкой выхода, которая не содержит инструкций ветвления или условий для раннего выхода.
У функции main есть один базовый блок, который принимает на вход все параметры, переданные в функцию, и содержит весь её код, так как в нём нет ветвлений:
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
Можно считать, что каждая область видимости, ограниченная фигурными скобками, является отдельным базовым блоком. Допустим, что код содержит ветвление:
// before
if 2 > 5 {
// true
} else {
// false
}
// after
В этом случае будет сгенерировано как минимум 4 базовых блока для:
- кода до ветвления,
- случая, когда выражение верно,
- случая, когда выражение ложно,
- кода после ветвления.
cond_br — инструкция для условного перехода. Если значение псевдорегистра %14 равно true, то выполняется переход в блок bb1. Если нет, то в bb2. br — безусловный переход, запускающий выполнение указанного базового блока:
// 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
Исходный код:
SIL guaranteed transformations
Сырое промежуточное представление, которое было получено на прошлом этапе, анализируется на корректность и трансформируется в каноничное: функции, помеченные transparent, инлайнятся (вызов функции подменяется её телом), вычисляются значения константных выражений, выполняется проверка на то, что функции, которые возвращают значения, делают это во всех ветвлениях кода и так далее.
Эти преобразования являются обязательными и выполняются, даже если оптимизация кода отключена.
Генерация каноничного SIL
Для генерации каноничного SIL используется флаг -emit-sil:
swiftc -emit-sil main.swift
Результат выполнения команды:
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'
В таком простом примере изменений немного. Чтобы увидеть реальную работу оптимизатора, нужно немного усложнить код. Например, добавить сложение:
let x = 16 + 8
В его сыром SIL можно найти сложение этих литералов:
%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
А в каноничном его уже нет. Вместо этого используется константное значение 24:
%4 = integer_literal $Builtin.Int64, 24 // user: %5
Исходный код:
SIL optimization
Дополнительные Swift-специфичные трансформации применяются, если включена оптимизация. Среди них специализация дженериков (оптимизация дженерик-кода под конкретный тип параметра), девиртуализация (замена динамических вызовов статическими), инлайнинг, оптимизация ARC и многое другое. Объяснение этих техник не влезает и в без того разросшуюся статью.
Исходный код:
Так как SIL – это особенность Swift, я не показывал в этот раз примеры реализации. К компилятору скобок мы вернёмся в следующей части, когда будем заниматься генерацией LLVM IR.