Swift і час компіляції

Пост заснований на статті medium.com/@RobertGummesson/regarding-swift-build-time-optimizations-fc92cdd91e31 + невеликі зміни/доповнення, тобто текст йде від мого обличчя, не третього.

Всім знайома ситуація після зміни файлу(ів) або просто перевідкриття проекту:






Потрібно сказати, що якоїсь особливої проблеми з компіляцією я не помічав коли писав на Objective-C, але все змінилося з приходом в маси swift. Я частково відповідав за CI сервер по збірці iOS проектів. Так от, проекти на swift збиралися нестерпно повільно. Ще розробники люблять тягнути поди (cocoapods залежності) на кожен чих у свої swift проекти, іноді, в шаленому кількості. Так от, компіляція всієї цієї суміші могла тривати кілька хвилин, хоча сам проект складався буквально з пари десятків класів. Ну гаразд, як би ясно, мова новий, на етапі компіляції відбувається набагато більше перевірок, ніж у тому ж ObjC, нерозумно очікувати, що вона буде працювати швидше (так, swift це суворо типізований мову і все повинно бути максимально явно оголошено (як в Java, наприклад), на відміну від ObjC, де не все так строго). Розробники swift з кожним релізом обіцяють в x раз прискорення швидкості компіляції. Ну начебто справді, збірка 2.0 і вже потім 2.2 стала працювати швидше, ось-ось на носі вже 3.0 версія (кінець року).

Компілятор компілятором, але виявляється, що час складання проекту можна катастрофічно збільшити буквально кількома рядками коду. Частина прикладів взято з вищезазначеної статті, частина самописні (не повірив авторові хотілося самому перевірити). Час заміряв через

time swiftc -Onone file.swift


Список того, що може сповільнити ваш компілятор

Необережне використання Nil Coalescing Operator

func defIfNil(string): String?) -> String {
return string ?? "default value"
}


Оператор ?? розгортає Optional, якщо там щось є, то повертає значення інакше виконується вираз після ??. Якщо зв'язати поспіль кілька цих операторів, то можемо отримати збільшення часу компіляції на порядок (не помилився, на порядок :) ).

// Без оператора компіляція займає 0,09 секунд
func fn() -> Int {

let a: Int? = nil
let b: Int? = nil
let c: Int? = nil

var res: Int = 999

if let a = a {
res += a
}

if let b = b {
res += b
}

if let c = c {
res += c
}

return res
} 


// З операторами 3,65 секунд
func fn() -> Int {

let a: Int? = nil
let b: Int? = nil
let c: Int? = nil

return 999 + (a ?? 0) + (b ?? 0) + (c ?? 0)
}


Так, у 40 разів :). Але мої спостереження показали, що проблема виникає, тільки якщо використовувати більше двох таких операторів в одному вираженні, тобто:

return 999 + (a ?? 0) + (b ?? 0)

вже буде ок.

Об'єднання масивів

Я іноді пишу скрипти на рубай, рубай приголомшливий мова, де велика кількість операцій можна укласти в пару рядків коду, без втрати читабельності. Особливо подобається працювати з масивами. Тобто, це цілком звичайний код на рубі для мене:

res = ar1.filter { ... } + ar2.map { ... } + ar3.flatMap { ... }


Якщо ж в цьому стилі напишемо на swift, то це може дуже сильно зіпсувати час компіляції, хоча конструкції мови дозволяють так писати. Ну і як всі зараз тільки й говорять про иммутабельности, тому створювати мутабельный масив не комільфо і взагалі поганий тон, пфф. Але що ж ми отримаємо, якщо будемо так писати?

// Час 0,15 секунд
func fn() -> [Int] {
let ar1 = (1...50).map { $0 }.filter { $0 % 2 == 0 }
let ar2 = [4, 8, 15, 16, 23, 42].map { $0 * 2 }

var ar3 = (1..<20).map { $0 }

ar3.appendContentsOf(ar1)
ar3.appendContentsOf(ar2)

return ar3
}


// Час 2,86 секунд
func fn() -> [Int] {
let ar1 = (1...50).map { $0 }
let ar2 = [4, 8, 15, 16, 23, 42]

return (1..<20).map { $0 } + ar1.filter { $0 % 2 == 0 } + ar2.map { $0 * 2 }
}


Різниця майже в 20 разів. А адже з масивами ми працюємо в кожному другому методі. Але варто відзначити, що така поведінка компілятора я отримав, коли підсумовую більше ніж два масиву в одному вираженні, тобто:

return ar1.filter { $0 % 2 == 0 } + ar2.map { $0 * 2 }

вже ок.

Ternary operator

Автор оригінального поста наводить приклад, з якого випливає, що не варто використовувати складні вирази разом з тернарным оператором:

// Build time: 0,24 секунди
let labelNames = type == 0 ? (1...5).map{type0ToString($0)} : (0...2).map{type1ToString($0)}

// Build time: 0,017 секунд
var labelNames: [String]
if type == 0 {
labelNames = (1...5).map{type0ToString($0)}
} else {
labelNames = (0...2).map{type1ToString($0)}
}


Надлишкові касти

Тут автор просто прибрав зайві касти CGFloat і отримав істотну швидкість компіляції файлу:

// Build time: 3,43 секунди
return CGFloat(M_PI) * (CGFloat((hour + hourDelta + CGFloat(minute + minuteDelta) / 60) * 5) - 15) * unit / 180

// Build time: 0,003 секунди
return CGFloat(M_PI) * ((hour + hourDelta + (minute + minuteDelta) / 60) * 5 - 15) * unit / 180


Складні вирази

Тут автор має на увазі, суміш локальних змінних, змінних класу і инстансов класу разом з функціями:

// Build time: 1,43 секунди
let expansion = a - b - c + round(d * 0.66) + e

// Build time: 0,035 секунд
let expansion = a - b - c + d * 0.66 + e


Правда тут автор оригінального поста так і не зрозумів чому так.




Загалом можна підвести підсумок, що використовувати складні фічі/конструкції мови це класно, але варто пам'ятати, що весь цей синтакчический цукор може суттєво вплинути як на час компіляції, так і на сам рантайм. Кажуть, якщо писати на скелі під андроїд, то за рахунок всього цього синтаксичного цукру, дуже швидко можна впертися в ліміт кількості методів і потрібно буде вдаватися до зайвих рухів.

пс.

Один з розробників swift мови дав невеликий відповідь на цей пост оригінальному автору, що в swift 3 зроблено багато удосконалень в цю сторону:
@matt_sven @NatashaTheRobot @RobertGummesson @iOSGoodies A lot of improvements made it into the Swift 3 branch after 2.x branched.  Joe Groff (@jckarter) 6 травня 2016 р.


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

0 коментарів

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