Устройство компилятора Swift. Часть 3


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

    • +16
    • 1,3k
    • 1
    e-Legion
    175,00
    Лидер мобильной разработки в России
    Поделиться публикацией

    Похожие публикации

    Комментарии 1

      0
      Пожалуйста продолжайте. Очень интересно.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое