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

    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."


    Перевод: "Проект был собран с оптимизациями — шаги в отладке могут вести себя странно; некоторые переменные могут отсутствовать".


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


    Во всем виноват параметр 'Fast', который неизменно идет с whole-module-optimization в настройках оптимизации Xcode. Нужно от него избавляться. Вернем оптимизацию на 'None', но теперь попробуем включить WMO через настройки проекта.


    Для этого нам достаточно добавить флаг SWIFT_WHOLE_MODULE_OPTIMIZATION=YES в user-defined-settings.


    Чтобы это сделать нужно зайти в build settings проекта и выбрать плюс на верхней панели. В выпавшем меню жмем на user-defined settings и прописываем значение обычной строкой:


    image


    Результат:


    image


    Время компиляции: 19 секунд. Изначально было 1.5 минуты.


    Скорость сборки значительно подпрыгнула и предупреждение во время отладки пропало. Тем не менее, полномодульная оптимизация накладывает определенные ограничения. Например, нельзя использовать флаг 'HEADERMAP_USES_VFS'(подсказал GxocT), который решает вопрос с инкрементальной компиляцией в некоторых случаях. Впрочем, по быстродействию то на то и выходит.


    Вывод: whole-module-optimization значительно ускоряет процесс компиляции, если вы не используете свои несовместимые с ним флаги.


    Бонус

    На правах шутки.


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



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


    Если верить stackoverflow, то в Xcode 8.2 эти проблемы будут исправлены. К сожалению, у меня проект на beta версии не собирается. Так что будем ждать официального релиза.




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


    В следующей статье рассмотрим способы починить инкрементальную компиляцию и общие способы ускорить проект.


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

    • +17
    • 9,5k
    • 4
    Поделиться публикацией

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

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

      +2
      Пробовали решение, которое было предложено сотрудником Apple?
      Не всех случаях помогает, но меня немного спасло.
      Xcode 8 does full project rebuild
        0
        Попробую, спасибо.
          0
          Проверил решение, действительно работает. Спасибо. Упомянул вас в статье.
          0
          Проблема действительно с выводом типов — старая, но хорошая статья по теме.

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

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