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

<img src=«cloud.kilograpp.com/f/26970d482b/?dl=1&t=1 alt=»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.0 ms
let labelNames = type == 0 ? (1...5).map{type0ToString($0)} : (0...2).map{type1ToString($0)}

// Build time: 16.9 ms
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)}

Час компіляції: ms 260. Поки все підтверджується.
Але мені здається, що тернарний оператор несправедливо звинувачений. Спробуємо знову розбити формулу на окремі вирази, але без використання 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.7 ms
let expansion = a - b - c + round(d * 0.66) + e

Час компіляції: 6ms
Додавання масивів:
// Build time Swift 2.2: 1250.3 ms
// Build time Swift 3.0: 92.7 ms 
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 найбільше гальмують складні формули. Сподіваюся, було корисно.
В подальшому ще пройдемося по аспектів мови, в тому числі перевіримо на швидкодію мовні структури switch-case, if-else, guard і так далі.
Буду радий зворотного зв'язку. Пишіть в коментарях, що разхабрать в першу чергу.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.