
Продолжаем изучать компилятор 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.
