Pull to refresh

Swift и время компиляции

Reading time 4 min
Views 25K
Пост основан на статье 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 сделано много улучшений в эту сторону:
Tags:
Hubs:
+20
Comments 56
Comments Comments 56

Articles