Тюнинг Swift компилятора. Часть 2

в 17:43, , рубрики: iOS, swift, xcode, Программирование, разработка мобильных приложений, разработка под iOS

image

Продолжение исследования способов ускорить компиляцию Swift.
Издевательство над семантическим анализатором и неожиданные настройки проекта.

Ссылка на первую часть для тех, кто пропустил.

Вступление

Доброго времени суток, господа разработчики. Хочу поблагодарить всех не обошедших стороной прошлый пост, было крайне приятно получить обратную связь. Надеюсь, эта статья вам понравится не меньше. Без долгих прелюдий скажу: сегодня не будет анализа быстродействия различных операндов типа guard и if-else, не будет скучной погони за нано-секундами в цикле на 100 000 итераций. У нас нашлось кое-что поинтереснее.

Старый баг лучше новых двух

Apple, я все починил, оно снова собирается 12 часов!
image

Шучу, просто неправильно указан тип. Там не массив, а словарь. Тем не менее, компилятор впадает в ярость от таких фокусов. Это плохой показатель, но в конце концов мы сами ошиблись.

Исправим ошибку. Поставим Dictionary вместо Array.
image

Кто выключил свет?
Ошибка сегментации, подсветка погасла, компилятор заглох.
С такими симптомами у нас крешится type-checker свифта, который никак не сможет сообразить какой ключ-значение мы от него ждем. Зайдя в отчет от сборке, мы видим stacktrace, который нам об этом говорит:
image

Хорошо. Что это нам дает и как это можно использовать?
Чтобы ответить на этот вопрос придется загрузить вас немного теорией:

Компилятор Swift состоит из нескольких модулей:

  • Парсер
    Парсит код в удобное для последующего разбора представление (AST). Это нужно, чтобы написанную вами кашу сделать читаемой для семантического анализатора.
  • Семантический анализ
    Разбирает полученное представление, делает его type-safe(!) AST каким-то особым яблочным колдунством.
  • SIL Generator
    Занимается генерацией промежуточного кода Swift и его оптимизацией. Промежуточный код — это код уже не понятный человеку, но еще не понятный машине. Зато в самый раз для компилятора.
  • LLVM IR Generation.
    Генерирует промежуточный код для самого LLVM, который нам всем наверняка знаком.
  • LLVM
    Создает непосредственно object файлы.

Графически эта последовательность выглядит так:

image

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

@Johan в комментарии к прошлой статье верно предположил, что проблема связана с выводом типов. А это задача как раз семантического анализатора. Все тормоза, которые мы наблюдали в прошлой статье, относятся именно к нему.

Итак, мы выяснили, что Swift болезненно воспринимает кастование типов. Теперь, когда мы знаем откуда ноги растут, давайте воспользуемся этим и явно укажем формат нашего словаря:

let myCompany: [String: [String: [String: String]]] = [
            "employees": [
                "employee 1": ["attribute": "value"],
                "employee 2": ["attribute": "value"],
                "employee 3": ["attribute": "value"],
                "employee 4": ["attribute": "value"],
                "employee 5": ["attribute": "value"],
                "employee 6": ["attribute": "value"],
                "employee 7": ["attribute": "value"],
                "employee 8": ["attribute": "value"],
                "employee 9": ["attribute": "value"],
                "employee 10": ["attribute": "value"],
                "employee 11": ["attribute": "value"],
                "employee 12": ["attribute": "value"],
                "employee 13": ["attribute": "value"],
                "employee 14": ["attribute": "value"],
                "employee 15": ["attribute": "value"],
                "employee 16": ["attribute": "value"],
                "employee 17": ["attribute": "value"],
                "employee 18": ["attribute": "value"],
                "employee 19": ["attribute": "value"],
                "employee 20": ["attribute": "value"],
            ]
        ]

Время компиляции: 30 мс. Было 90 мс. Ускорение в три раза.

Успех. Делаем вывод, что лучше всегда явно указывать типы, а не полагаться на смекалку компилятора.
Кстати, есть и обратная сторона. Если неправильно проставить тип, то пройдет много времени, прежде чем компилятор поймет куда вы его послали ¯_(ツ)_/¯

Еще может возникнуть вопрос, есть ли разница между литералом и явным указанием Dictionary/Array класса? Скажу сразу — это одно и тоже, на время компиляции и выполнения это никак не влияет. Но для читаемости рекомендую использовать именно литерал.

Немного веселого кода

Так как вложенные словари в Swift являются optional, то можно получить следующую пунктуацию:

var myCompany: [String: [String: String]?] = [
    "employees": [ "attribute" : "value"]
]

let Почему = myCompany["employees"]
print(Почему?!)

И это компилируется.

Еще больше скорости

Есть стереотип, что включение оптимизации замедляет сборку. Однако, если почитать документацию компилятора, то выясняется обратное.

По умолчанию, компилятор собирает индивидуально каждый файл. Если открыть лог сборки при дефолтных настройках, то будет видно отчет по каждому Swift файлу в отдельности:

image

В настройках оптимизации есть такой флаг как whole-module-optimization. При этом флаге компилятор рассматривает проект как единое целое, целиком видит все имеющиеся функции и экономит на лишних кастованиях типов.
В случаи компиляции с этим флагом сборка всех Swift классов объединяется в единую операцию:

image

Давайте теперь сравним быстродействие. Возьмем некий абстрактный тестовый проект. Пусть это будет одно из Open Source творений нашей компании. Он не отличается чистотой кода и гениальностью решений, что нам и нужно.

Соберем сначала без оптимизации:
image
Время компиляции: 84 секунды.

Теперь включим whole-module-optimization:
image
Время компиляции: 52 секунды. Выигрыш 40%!
Естественно, на производительности это тоже положительно отражается. По моему личному опыту, это дает ~10% прирост к общему быстродействию.

Как обычно, стоит задаться вопросом: "В чем подвох?". Когда запускаешь приложение и паркуешься на брейкпоинт, то дебаггер нам сообщает следующее:

image

Шрифтом брайля: "Project was compiled with optimization — stepping may behave oddly; variables may not be available."
Перевод: "Проект был собран с оптимизациями — шаги в отладке могут вести себя странно; некоторые переменные могут отсутствовать".

Речь о том, что при оптимизации удаляются неиспользуемые переменные и мертвый код, поэтому могут возникать странные провалы в отладке. Поработав пару дней над проектом с включенной оптимизацией, я столкнулся со сложностями в отладке только единожды, когда испытывал не привязанный к чему-либо код. Компилятор его просто удалил.
Если кто-то получит поведение о котором предупреждает отладчик, то буду рад услышать способы стабильно его воспроизвести.

Вывод: можно включить whole-module-optimization для значительного ускорения сборки, но на свой страх и риск.

Бонус

В комментариях к прошлой статье писали, что у Swift есть инкрементальная компиляция.
Для эксперимента предварительно собрал проект и добавил в существующий метод один единственный print. Вот что из этого получилось:

У кого нет возможности посмотреть: 50 секунд сборка с нуля, 45 секунд инкрементальная.


На этом вторую часть подытожим. В ней мы выяснили основную причину торможения компилятора, нашли флаги оптимизатора для ускорения сборки, а так же наблюдали несовершенство инкрементальной компиляции в Swift.

Благодарности:
GrimMaple — за консультации по строению компилятора и помощь в подборе формулировок.

Автор: Mehdzor

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js