Всем знакома ситуация после изменения файла(ов) или просто переоткрытия проекта:
Нужно сказать, что какой-то особой проблемы с компиляцией я не замечал когда писал на 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 г.
Комментарии (27)
EGlaz
07.05.2016 15:55+2Интересно как влияет синтаксис на время работы приложения. Если компиляция дольше, но работа быстрее — оно того стоит.
khim
08.05.2016 17:02+4Когда как. Я как-то занимался тем, что переделал компонент так, что он стал работать в 10 раз быстрее — но стал компилироваться 30 минут. Всё бы ещё ничего и можно было бы с этим жить, но выяснилось, что «малые шевеления» кода могут его ускорять/замедлять вдвое-вдрое (естественно без выдачи каких-либо сообщений куда либо… лишнее это), а заметить это можно только по резкому ускорению времени компиляции (похоже после перехода через некий внутренний порог MSVC просто решал «а, фиг с ним — к чёрту оптимизации, они только мешают»)!
В общем пришлось всё переделать и замедлить код в 1.5 раза — но сделать так, чтобы он и с MSVC нормально работал, а не только с GCC/Clang'ом. Было обидно, да.
P.S. Сейчас я бы, наверное, постарался убедить всех просто использовать clang, но несколько лет назад это выходом не было: он был тогда ещё слишком сырым под Win64…
alien007
09.05.2016 15:12+1Смысл тут наверное не столько в скорости работы приложения, сколько в обспечиваемой безопасности разрабатываемых на них программ (с малым ущербом для производительности). Rust, Go и Swift относятся к группе нового поколения memory-safe языков, которые обеспечивают дополнительный уровень безопасности во время компиляции (нарушение иммутабельности, spatial и temporal memory access violation), чего не было реализовано в том же C++. Время компиляции последнего кстати от этого тоже сильно страдает на больших проектах.
stargazr
07.05.2016 16:43+5Они наверняка на С++ ничего не писали… :)
Какая-то сомнительная идея жертвовать выразительностью кода ради пары минут.
Пример «гора if против ??» ужасен. Лучше покомпилировать еще минуту, чем наблюдать такое.
Я уж не говорю о том, что неплохо было бы сравнить скорости компиляции полноценных программ, содержащих такие конструкции, а не этих обрывков. «На порядок медленее» может и затеряться на фоне остального кода, который тоже нужно компилировать.
vsb
07.05.2016 17:48+3Скорее всего это баги компилятора. Нет никаких разумных причин так замедлять компиляцию в этих местах. Вообще Swift мне показался сделанным наспех. Концепция хороша, а реализация как у студенческой курсовой. Тот же Xcode вначале на некоторых участках кода падал каждые несколько секунд. Постепенно вылизывают. Думаю пока не время на него переходить, сырой продукт, лучше подождать лет 5, тем более, что Objective C с каждым релизом становится всё лучше.
encyclopedist
07.05.2016 20:44+2Да, даже трудно представить, что там должно происходить чтобы добавление одного слагаемого замедляло компиляцию в десятки раз. Такое впечатление, что внутри где-то экоспоненциальный, или даже сверхэкспоненциальный (факториал) алгоритм.
nickolaym
07.05.2016 22:02Может, баги компилятора, а может, спецификация языка такая, что прямо заставляет компилятор налетать на какой-нибудь комбинаторный взрыв. Скажем, не тупо выводить тип выражения по правилам (как в С++ либо как в ML / Haskell), а пытаться найти сочетание, которое удовлетворит всех наилучшим образом — ну, не знаю, перебирать все известные числовые (в т.ч. плавающие) типы для каждого целочисленного литерала.
(Теоретизирую, потому что сам в свифт нос не засовывал).
AcidLynx
07.05.2016 23:19-11Как и обычно. Чем проще контрукции использовать (if-else вместо тернарного оператора, например), то и компиляция быстрее происходит, и понятнее код становится.
Вывод — пишите понятнее для себя и других. Тогда и компилятор не будет «зависать».Prototik
07.05.2016 23:27+13Вывод здесь один — компилятор Swift дерьмо. Никто не должен как либо ограничивать меня или кого-то ещё в стиле программирования. Как хочу — так и пишу. Если время компиляции растёт в десятки раз от простейших вещей — то проблема не в этих простейших вещах, очевидно же.
6opoDuJIo
08.05.2016 13:09Даже не так: если фича сделана через жопу, то и толк от этой фичи малоощутим. Что и наблюдается.
khim
08.05.2016 21:29+2Очень хорошо перекликается с соседней статьёй: разработчики компилятора Swift'а оправдываются тем, что у них гораздо более сложная задача и сделать её быстро — не так-то просто.
Возможно. Но. Кто их просил делать такие фичи в языке, что использование простейших, совершенно не «узободробительных» конструкций выливается во многочасовою компиляцию? Марсиане?
С++ тоже этим страдает во многом, но там всё-таки уж самые ужасные болячки победили, а тут… просто праздник какой-то…
sergeylanz
08.05.2016 12:39-4я думал swift когда нибуть на сервер придёт… но таким времени компиляции спасибо не надо, буду дальше на GO писать
korniltsev
08.05.2016 13:23+2Большие android проекты собираются в разы дольше. :( мои последние 2 проекта собираются(инкрементально — изменение одного java файла) 1м13с и 47с. И еще секунд 7 устанавливаются на девайс. И это на топовом макбуке.
6opoDuJIo
08.05.2016 15:34Объём бы знать.
korniltsev
08.05.2016 16:22+11м13с: 2560 классов не считая библиотек, Всего: 160k методов, 7.9M classes.dex, 8.5M classes2.dex, 3.4M classes3.dex.
DevAndrew
09.05.2016 14:23Вы пробовали использовать Instant Run?
В скорости компиляции моих проектов это очень помогло.korniltsev
09.05.2016 14:442 месяца назад не работал совсем. Сейчас проверил — работает. Изменение тела метода -> 17 секунд. Добавление метода — 27 секунд.
XanKraegor
08.05.2016 16:14Swift развивается, притом усилиями не только самой Apple, но и сообщества, и это хорошо. Например, до версии 2.2 даже простая арифметика типа
let sunTrueLongitude: Double = meanAnomaly + 1.916 * sin(meanAnomaly * d2r) + 0.020 * sin(2 * meanAnomaly * d2r) + 282.634
требовала по неизвестной для меня причине разбиения на несколько фрагментов. Теперь уже нет. Также присоединяюсь к комментарию EGlaz: считаю, что в разумных пределах время компиляции не так важно, как скорость работы готовой программы.
a553
А как же любимый вариант:
sleeply4cat
А что не так с этим вариантом в плане времени компиляции?..
a553
Ну например то, что этот кусок кода компилируется 12 часов.
int00h
> Ну например то, что этот кусок кода компилируется 12 часов.
Точнее компилировался Resolved: 26 Apr 2016
Speakus
Точнее компилируется. Xcode 7.3.1 (последняя на данный момент версия, опубликована 3 мая) всё ещё зависает на компиляции такого кода. А откуда у Вас инфа про resolved?
encyclopedist
https://github.com/apple/swift/commit/2cdd7d64e1e2add7bcfd5452d36e7f5fc6c86a03
Если пройти по ссылке в комментарии a553, то там вся нужная информация.
encyclopedist
Сам баг тут: https://bugs.swift.org/browse/SR-305
house2008
Да) был подобный случай, только на ObjC, на CI сервере кланг завис минут на 30, потом в сегфолт по памяти свалился :) А самое интересное было, что это только в релиз режиме (xcodebuild -configuration Release) происходило, то есть у разработчика на машине работает всё окей, но на сервере падает.