image


Обзор Swift 3 компилятора и способы его ускорить. Часть 1.
Развенчание существующих мифов. Мнение о проблемах autocompletion в Xcode.



Предисловие:


Наша компания занимается разработкой мобильных приложений под ключ. Многие наши iOS разработчики говорят на Objective-C лучше, чем на русском, их девушка Cocoa, а спят они в обнимку с айфоном… и вот стали мы вдруг писать на Swift.


Я не буду говорить про различные косяки синтаксиса, веселые "Segmentation Fault: 11", периодически гаснущую подсветку, это все и так известно. Пусть больно, но терпимо.
Но есть кое-что по-настоящему убивающее бизнес, а не просто доставляющее дискомфорт. Медлительный компилятор. Да-да, это не просто громкий заголовок.
Когда одинаковые по объему проекты на Obj-C и Swift собираются с четырехкратной разницей во времени. Когда при добавлении одного метода стартует пересборка половины всего кода. Когда ошибки компилятора вообще выводят его из строя — это настоящее убийство времени разработчика. А как известно: время — это деньги.


Есть два варианта: продолжить ныть и терпеть, либо решать вопрос. Мы выбрали второе.


Изобретение велосипеда


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


Так зачем же еще одну плодить? А затем, что, во-первых, все это было еще до третьего свифта, во-вторых, некоторые утверждения в статье не совсем верны, а так же список коварных мест было бы неплохо дополнить. Чем мы и займемся.


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


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


Nil Coalescing Operator


Моя любимая фишка Swift, сахарный optional. Чем-то похож на nil-safe сообщения в Obj-C.


Возьмем пример из прошлых статей. Сейчас вы поймете, почему они не совсем корректны:


let left: UIView? = UIView()
let right: UIView? = UIView()
let width: CGFloat = 10
let height: CGFloat = 10

let size = CGSize(width: width + (left?.bounds.width ?? 0) + (right?.bounds.width ?? 0) + 22, height: height)

Время компиляции: 12 секунд! Приятель, у тебя третий пень что ли?
Даже хуже, чем было в Swift 2.2.


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


let firstPart = left?.bounds.width ?? 0 + width
let secondPart = right?.bounds.width ?? 0 + 22
let requiredWidth = firstPart + secondPart
let size = CGSize(width: requiredWidth, height: height)

Время компиляции: 30 ms. (миллисекунд)


Получается, дело вовсе не в злых optional?
Но нет, это было бы слишком просто. Давайте усложним задачу:


class A {
    var b: B? = B()
}

class B {
    var c: C? = C()
}

class C {
    var d: D? = D()
}

class D {
    var value: CGFloat? = 10
}

...

let left: A? = A()
let right: A? = A()
let width: CGFloat = 10
let height: CGFloat = 10

// Опциональная ламбада! 
let firstPart = left?.b?.c?.d?.value ?? 0 + width
let secondPart = right?.b?.c?.d?.value ?? 0 + 22
let requiredWidth = firstPart + secondPart
let size = CGSize(width: requiredWidth, height: height)

Время компиляции: 35 ms.


Вывод: У Nil Coalescing Operator все стерильно, можно пользоваться.
Но тогда в чем же была проблема?


Уже не сложно догадаться, что корень зла таится в длинных выражениях. Автор русской статьи вскользь упомянул, что проблема с nil coalescing operator воспроизводится только в сложных операциях, но, к сожалению, не заострил на этом внимание.


Правило следующее: у компилятора вызывают запор выражения с несколькими сложными слагаемыми. То есть теми, которые не просто являются переменными, но и выполняют какие-либо действия. А вот складывать переменные можно сколько угодно.


Вы, наверное, скажете: "Где пруфы, Билли?"


Хорошо. Тогда возьмем предыдущий код, но не будем дробить его на под-операции:


let requiredWidth = left?.b?.c?.d?.value ?? 0 + right?.b?.c?.d?.value ?? 0 + width + 22
let size = CGSize(width: requiredWidth, height: height)

Результата долго ждать не пришлось (пришлось):
image


Цитирую, если не получилось прочитать со скрина: "Expression was too complex to be solved in reasonable time; consider breaking up the expression into distinct sub-expressions".


Перевод: "Выражение было слишком сложным, чтобы решить за приемлемое время. Разбейте формулу на отдельные под-выражения."


Ч.т.д.


Неожиданный сверх-эффект

Дальнейшее является наблюдением без теоретической базы.


Многие замечали, что в Xcode регулярно отваливается auto-completion. Это, как правило, происходит в момент фоновой компиляции. Если вы написали что-то вроде выражения, которое вызывает "Expression was too complex", то сразу за этим умрут и подсказки.


Это можно легко проверить. Возьмем тот же метод и начнем писать self.view, чтобы получить подсказку:
image


А потом добавим наше выражение-убийцу. Все, подсказок вы больше не получите, даже если усиленно лупить по ctrl+space:
image


Лечится это запуском явной компиляции и устранением ракового кода.


Идем дальше.


Тернарный оператор


В статье так же освещаются проблемы тернарного оператора. Время компиляции кода можно увидеть в комментариях:


// Build time: 239.0ms
let labelNames = type == 0 ? (1...5).map{type0ToString($0)} : (0...2).map{type1ToString($0)}

// Build time: 16.9ms
var labelNames: [String]
if type == 0 {
    labelNames = (1...5).map{type0ToString($0)}
} else {
    labelNames = (0...2).map{type1ToString($0)}
}

Кстати, у меня такого метода как type0ToString в SDK не нашлось. Я его заменил на упрощенный вариант, разницы никакой:


let labelNames = type == 0 ? (1...5).map{String($0)} : (0...2).map{String($0)}

Время компиляции: 260 ms. Пока все подтверждается.


Но мне кажется, что тернарный оператор несправедливо обвинен. Попробуем снова разбить формулу на отдельные выражения, но без использования if-else:


let first = (1...5).map{String($0)}
let second = (0...2).map{String($0)}
let labelNames = type == 0 ? first : second

Время компиляции: 45 ms


Но это не предел. Упростим еще больше:


let first = 4
let second = 5
let labelNames = type == 0 ? first : second

Время компиляции: 7 ms.


Вердикт: тернарный оператор оправдан.


Еще несколько амнистий


Операция Round():


// Build time: 1433.7ms
let expansion = a - b - c + round(d * 0.66) + e

Время компиляции: 6ms


Сложение массивов:


// Build time Swift 2.2: 1250.3ms
// Build time Swift 3.0: 92.7ms 
ArrayOfStuff + [Stuff]

Время компиляции: 19ms


И самое сладкое:


let myCompany = [
            "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"],
            ]
        ]

Время компиляции: 86 ms. Могло быть и лучше, но уже хотя бы не 12 часов.




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


Вторая часть.

Какую версию Swift используете вы?

Проголосовало 229 человек. Воздержалось 69 человек.

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

Поделиться с друзьями
-->

Комментарии (44)


  1. Deosis
    07.12.2016 06:36

    Во-первых, почему в опросе нет варианта «не использую»?
    Во-вторых, в С++ и С# тернарный оператор ленивый и вычисляет одно выражение. Зачем его использовать, если необходимо вычислять ветки заранее?


    1. ad1Dima
      07.12.2016 07:35

      Использовать, когда оба операнда представлены простыми логико-арифметическими операциями.


    1. Mehdzor
      07.12.2016 09:09

      1. Вариант 'воздержался'.
      2. Есть предположение, что Swift оптимизирует предварительное вычисление. Проверю в следующей статье.


      1. Jef239
        07.12.2016 23:38

        Скорее оптимизируются проверки операндов на NULL и прочие исключение. Повертите ручки, отключающие оптимизацию — они должны сильно ускорить компилятор.


  1. NeoCode
    07.12.2016 08:09
    +3

    Простите, это как? После стольких лет развития компиляторов они не могут распарсить длинное (да не такое уж и длинное) выражение??? Почему аналогичное выражение на С/С++ (да и на любом другом языке) парсится без каких-либо проблем?
    Кто там у них компиляторы пишет?


    1. basili4
      07.12.2016 08:47
      -4

      В apple компиляторы не пишут. Их там рисуют. Ну еще момент если хипстеры должны страдать то почему c разрабами под ios должно быть по другому?


    1. Mehdzor
      07.12.2016 09:16
      +1

      Кто у них пишет Xcode! Вот что надо спросить.


      1. Mehdzor
        07.12.2016 09:19
        +3

        А потом расстрелять.


        1. basili4
          07.12.2016 09:23
          -3

          Мне кажется расстрел это слишком гуманно. Может стоило из самих заставить писать код на Xcode пожизненно.


          1. Alexeyuts
            07.12.2016 10:57
            -1

            Писать код Xcode на Xcode :)


    1. TargetSan
      07.12.2016 12:29

      Я почему-то подозреваю, что трабл у них начинается не при парсинге выражения, а на стыке вывода типов и разрешения перегрузок. Но то, что ребята не освоили какого-нибудь Хиндли-Милнера, не извиняет их.
      Кстати, может чего упустил, но вроде эта проблема была ещё во 2-й версии, и её клятвенно обещали пофиксить.


      1. Johan
        07.12.2016 13:01
        +1

        Мои наблюдения, не подтвержденные никакими исследованиями, говорят о том, что проблемой является вывод типов. Причём, дело не только в выводе дженериков по возвращаемому значению (как я понимаю, процесс трудоемкий, потому эта фича в «классических» языках отсутствует), но даже в числах, ибо числовые литералы в Swift не имеют типа, тип определяется исходя из контекста (такого в «классических» языках тоже нет). Как-то так выглядит проблема с моего дивана.


        1. ad1Dima
          07.12.2016 13:25

          Что именно понимается под выводом Дженериков по возращаемому значению? Разве весь Linq в C# на этом не работает?


          1. Johan
            07.12.2016 15:43

            Имеется ввиду не тип возращаемого значения делегата, а тип возвращаемого значения самого метода наример. Если написать метод с дженерик возвращаемым типом и без параметров, то в C# при вызове прийдется явно указывать этот тип, даже при присваивании результа в переменную с конкретным типом. Swift же умеет выводить такие типы. Сюдаже, кстати, можно отнести перегрузки по возвращаемым типам, которы тоже встречаются нечасто, как правило, перегрузки работают только по параметрам.


            1. ad1Dima
              07.12.2016 17:03

              var res = items.Select(o => new Tuple<string, int>(o.Value.ToString(), o.Value));
              


              Метод Select<T>() — дженерик. Возвращаемое значение вычислено — IEnumerable<Typle<string,int>>. Или вы про что-то другое?


              1. Johan
                07.12.2016 18:42

                Тут возвращаемое значение метода Select вычисляется благодаря параметру типа Func<T, Tuple<string, int>. Я же говорю о следующей ситуации:


                public interface IMessage
                {
                    string Text { get; set; }
                }
                
                public static class MessageFactory
                {
                    static T Create<T>(string message) where T: IMessage, new()
                    {
                        var result = T();
                        result.Message = message;
                        return result;
                    }
                }

                Для вызова метода Create необходимо явно указать тип в самом вызове:


                var message1 = MessageFactory.Create<SimpleMessage>("Hello world")
                SimpleMessage message2 = MessageFactory.Create<SimpleMessage>("Hello world again")

                Swift, в свою очередь, позволяет сделать следующее:


                protocol Message {
                    init()
                    var text: String { get set }
                }
                
                class MessageFactory {
                    static func create<T: Message>(text: String) -> T {
                        var result = T()
                        result.text = text
                        return result
                    }
                }
                
                let message: SimpleMessage = MessageFactory.create(text: "Hello world")

                Как видите, тип не фигурирует в вызове, только в объявлении переменной. То есть, в данном случае, компилятор не тип выражения выводит исходя из того, что возвращает функция, а тип возвращаемого значения выводит из того, в каком выражении участвует вызов.


                1. ad1Dima
                  08.12.2016 14:43

                  Насколько я понимаю, тут вся фишка в возможности наследования для анонимных типов, которой нет в C# (а жаль)


    1. Jef239
      07.12.2016 23:37

      Это явно не парсер, а оптимизатор трудится. Судя по времени — делается полная проверка кода, формирующего операнды. Как гипотеза — на предмет, могут ли операнды вызвать исключение.


      1. Mehdzor
        08.12.2016 00:12

        Оптимизация отключена. Вы же о флаге -Onone?


        1. Jef239
          08.12.2016 14:00

          я никогда не работал с Swift, просто в свое время участвовал в нескольких простых компиляторах. Парсер просто не может занимать такое время. Чуть выше Johan выдал весьма правдоподобную гипотезу о выводе типов. Это тоже в некотором смысле оптимизация, можно было скомпилировать ветки для всех возможных типов. А пытаться понять, какие именно типы аргументов возможны — это реально долго.


  1. rule
    07.12.2016 08:18
    +4

    А где тюнинг компилятора? Я думал вы исходники поправите самого компилятора, а не свои исходники.
    Swift3 компилятору всего один год, как и парсеру. Некоторые проблемы существуют, но они не критичные, особенно учитывая что файлы уже собранные не пересобираются. Сборка во время разработки инкрементальная. Поэтому это всё «всплывает» при сборке проекта «с нуля». Если сильно раздражает — добро пожаловать в список котрибьютеров: https://github.com/apple/swift


    1. Mehdzor
      07.12.2016 09:25
      -2

      Если сильно раздражает — добро пожаловать в список котрибьютеров: https://github.com/apple/swift

      Уже отправил резюме в Apple.


      1. Mehdzor
        07.12.2016 09:50
        -2

        (нет)


      1. rule
        07.12.2016 10:26
        +2

        чтоб контрибутить в свифт, не нужно быть работником Apple. Swift — опен сорсный проект. Пишите — создавайте PR.


      1. AKhatmullin
        07.12.2016 10:37
        +1

        Swift уже давно на совести open source сообщества. (https://swift.org)


        1. ad1Dima
          07.12.2016 12:03
          -1

          Давайте будем честны, если открыть последние пулреквесты, то там только 1 или 2 человека не сотрудники apple.


          1. AKhatmullin
            07.12.2016 16:34

            А разве это не логично? Apple — первые из тех кто заинтересован в развитии языка.
            Теперь продолжим игру в честность — у вас ведь есть возможность внести свой вклад? Так почему вы этого не делаете?


            1. ad1Dima
              07.12.2016 16:39

              А я говорю, что это плохо? Я говорю, что swift развивают люди за зарплату Apple. Вот такая «совесть open source сообщества».

              И да, это логично, потому что чтоб контрибьютить в такие проекты нужно обладать огромным количеством знаний и времени. Мало кто будет делать это бесплатно.

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


    1. Mehdzor
      07.12.2016 09:36

      Так вот инкрементальная сборка срабатывает только для изменений внутри метода. А если добавляешь новый метод или еще что, то собираются вдобавок и зависимые файлы повторно. Это самое грустное отличие от Obj-C. Фаст-пруф.


      Кроме того, он еще и баганутый.


      Тем не менее, спасибо за ответ. Инкрементальную компиляцию рассмотрю в одной из следующих статей.


      1. rule
        07.12.2016 10:25

        он пересобирает только изменившийся файл, а не весь проект — это хорошо.
        Инкрементальная не срабатывает при изменении параметров компиляции, это не проблема самого механизма инкрементальной сборки, это проблема XCode.
        Если собирать с командной строки, такого не будет.


        1. Mehdzor
          07.12.2016 10:41

          К сожалению, разрабатываем мы именно в Xcode. Сам регулярно сталкиваюсь с проблемой, что собирается больше, чем один файл. Постараюсь разобрать вопрос инкрементальной компиляцией в следующий раз.


          1. rule
            07.12.2016 11:46

            Так я не предлашаю вам отказаться от XCode, просто swift — это отдельный проект, от икскода не зависящий. А статья называется «тюнинг компилятора». Ну и вы пишите что это проблема компилятора.


            1. Mehdzor
              07.12.2016 12:04

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


  1. bududomasidet
    07.12.2016 09:03

    Спасибо за статью… Теперь я понимаю, почему мой проект долго собирается… И дело вовсе не в подах(((


    1. Mehdzor
      07.12.2016 09:14

      Рекомендую использовать Carthage вместо Cocoapods, где это возможно.
      Есть даже перевод их туториала.


      1. RomanVolkov
        07.12.2016 09:38

        Как раз хотел спросить. Я сам прихожу к тому чтобы использовать Carthage вместо Cocoapods. Но какой плюс с точки зрения сборки проекта? Например, в XCode поды собираются один раз, а пересобираются только после clean.


        1. Mehdzor
          07.12.2016 09:43

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

          Краткая суть Carthage — он собирает предварительно framework из твоей зависимости(чем-то похоже на флаг !use_frameworks в Cocoapods), а потом достаточно ее просто подключить к проекту.


          1. RomanVolkov
            07.12.2016 10:06

            Да, я пользовался Carthage и кроме этого плюса я пробую найти еще.
            Другой наверное (но небольшой) — XCode'у не приходится индексировать исходники подов (и в Quick Open они не показываются)
            Но, может быть кто-то замерял, с чем быстрее идет инкрементальный билд (в случае когда поды собраны): с carthage или cocapods


  1. AKhatmullin
    07.12.2016 11:07
    -1

    Вообще материал занимательный и полезный, но:
    1. Пример с вычислением размера — такие упрощения в коде полезно делать не только для ускорения компиляции, но и для лучшей читаемости.
    2. Уже конечно давно принято винить Apple во всем от глюков Xcode и убийства Кеннеди до неудачного расположения города Помпеи, но если все так плохо с инструментарием предоставленным Apple, то почему не используете Xamarin и ему подобные?


    1. Mehdzor
      07.12.2016 11:25

      1. +1
      2. Много альтернативных платформ пробовали, объем third-party фреймворков там значительно меньше, чем у нативной разработки. Как следствие, помощи в гугле тоже меньше.
      Если отходить от Xamarin и говорить в общем, то еще вылезают сложности с оптимизацией. Часто бывает, что сам движок добавляет ощутимый overhead, который никак не разгонишь.

      Пока яблочное зло — наименьшее. Периодически еще тыкаемся в Appcode, но разочаровываемся. Надеюсь, Jetbrains подтянут его.

      Но то что яблоки лучший вариант на рынке, это не значит, что так должно быть! Я параллельно разрабатываю backend на Java/Kotlin, так там вообще практически не к чему придраться. Такого качества я жду от Apple.


      1. AKhatmullin
        07.12.2016 12:16

        Но то что яблоки лучший вариант на рынке, это не значит, что так должно быть!

        Вот тут пожалуй плюсану. За последние три года качество продуктов Apple резко упало вниз.


    1. ad1Dima
      07.12.2016 12:06

      Скажу по секрету, Xamarin почти не избавляет от проблем эпловких тулов, потому что сам через них работает. К тому же добавляет свои проблемы.


  1. AKhatmullin
    07.12.2016 12:16

    (ой не там)


  1. KvanTTT
    07.12.2016 14:43

    Правило следующее: у компилятора вызывают запор выражения с несколькими сложными слагаемыми. То есть теми, которые не просто являются переменными, но и выполняют какие-либо действия. А вот складывать переменные можно сколько угодно.

    Вспомнилась проблема с производительностью в генераторе парсеров ANTLR. Там она как раз была связана с большим количеством однородных выражений при использовании леворекурсивных правил (пример с конкатенацией для Java.g4 грамматики: ManyStringConcatenation.java). Для решения этой проблемы такие правила нужно было переделывать, что выглядело не очень красиво и удобно. У результирующего парсера также появлялись проблемы с производительностью и при полном отказе от использования леворекурсивных правил. Если кому-то интересно, то о решении этих проблем я писал в своей статье о теории и практике парсинга исходников с помощью ANTLR и Roslyn в разделе Java- и Java8-грамматики.


    К счастью, в настоящий момент проблема с леворекурсивными правилами пофикшена в ANTLR, а исправление будет доступно с версии 4.6, в которой, к слову, появится еще и поддержка Swift-рантайма.


    Интересно, в чем же заключается проблема текущей версии Swift парсера и как ее можно разрешить?