Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров12K

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 используете вы?
2.57% Swift 2.27
5.15% Swift 2.314
71.69% Swift 3.0195
20.59% Objective-C56
Проголосовали 272 пользователя. Воздержались 79 пользователей.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 24: ↑22 и ↓2+20
Комментарии44

Публикации

Истории

Работа

Swift разработчик
31 вакансия
iOS разработчик
24 вакансии

Ближайшие события