Pull to refresh

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

Reading time5 min
Views12K

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 больше всего тормозят сложные формулы. Надеюсь, было полезно.


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

Only registered users can participate in poll. Log in, please.
Какую версию Swift используете вы?
2.57% Swift 2.27
5.15% Swift 2.314
71.69% Swift 3.0195
20.59% Objective-C56
272 users voted. 79 users abstained.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+20
Comments44

Articles

Change theme settings