Почему время компиляции Swift так медленно?

Я использую Xcode 6 Beta 6.

Это то, что беспокоило меня в течение некоторого времени, но сейчас оно достигло точки, когда его едва ли можно использовать.

Мой проект начинает иметь приличный размер 65 файлов Swift и несколько мостовых файлов Objective-C (которые на самом деле не являются причиной проблемы).

Кажется, что любое небольшое изменение любого файла Swift (например, добавление простого пробела в классе, который едва используется в приложении) приведет к перекомпиляции всех файлов Swift для указанной цели.

После более глубокого исследования я обнаружил, что 100% времени компилятора занимает CompileSwift фаза, где XCode запускает swiftc Команда на всех файлах Swift вашей цели.

Я провел дополнительное исследование, и если я оставлю делегат приложения только с контроллером по умолчанию, компиляция будет очень быстрой, но, поскольку я добавлял все больше и больше файлов моего проекта, время компиляции начало становиться очень медленным.

Теперь, имея только 65 исходных файлов, каждый раз для компиляции требуется около 8/10 секунд. Не очень быстро.

Я не видел ни одного поста, рассказывающего об этой проблеме, кроме этой, но это была старая версия Xcode 6. Поэтому мне интересно, единственный ли я в этом случае.

ОБНОВИТЬ

Я проверил несколько проектов Swift на GitHub, таких как Alamofire, Euler и CryptoSwift, но ни у одного из них не было достаточно файлов Swift для реального сравнения. Единственный проект, который я обнаружил, который имел приличный размер, был SwiftHN, и, хотя он имел только дюжину исходных файлов, я все еще был в состоянии проверить одно и то же, один простой пробел и весь проект нуждался в перекомпиляции, которая начинала принимать мало времени (2/3 секунды).

По сравнению с кодом Objective-C, где анализатор и компиляция работают быстро, на самом деле кажется, что Swift никогда не сможет обрабатывать большие проекты, но, пожалуйста, скажите, что я ошибаюсь.

ОБНОВЛЕНИЕ с Xcode 6 Beta 7

Все еще никакого улучшения вообще. Это начинает становиться смешным. С отсутствием #import в Swift я действительно не понимаю, как Apple сможет когда-либо оптимизировать это.

ОБНОВЛЕНИЕ С Xcode 6.3 и Swift 1.2

Apple добавила инкрементные сборки (и многие другие оптимизации компилятора). Вы должны перенести свой код в Swift 1.2, чтобы увидеть эти преимущества, но Apple добавила инструмент в Xcode 6.3, чтобы помочь вам сделать это:

Введите описание изображения здесь

ТЕМ НЕ МЕНИЕ

Не радуйся так быстро, как я. Решатель графиков, который они используют для создания инкрементальной сборки, еще не очень хорошо оптимизирован.

Действительно, во-первых, он не рассматривает изменения сигнатуры функции, поэтому, если вы добавите пробел в блоке одного метода, все файлы, зависящие от этого класса, будут перекомпилированы.

Во-вторых, кажется, что создается дерево на основе файлов, которые были перекомпилированы, даже если изменение не влияет на них. Например, если вы переместите эти три класса в разные файлы

class FileA: NSObject {
    var foo:String?
}
class FileB: NSObject {
    var bar:FileA?
}
class FileC: NSObject {
    var baz:FileB?
}

Теперь, если вы измените FileAКомпилятор явно пометит FileA быть перекомпилированным. Это также перекомпилирует FileB (это было бы хорошо на основе изменений в FileA), но также FileC так как FileB перекомпилируется, и это очень плохо, потому что FileC никогда не использует FileA Вот.

Поэтому я надеюсь, что они улучшат этот решатель дерева зависимостей... Я открыл радар с этим примером кода.

ОБНОВЛЕНИЕ С Xcode 7 beta 5 и Swift 2.0

Вчера Apple выпустила бета-версию 5, и внутри заметок о выпуске мы могли видеть:

Swift Language & Compiler • Инкрементные сборки: изменение только тела функции больше не должно приводить к перестроению зависимых файлов. (15352929)

Я дал ему попробовать, и я должен сказать, что он работает действительно (действительно!) Хорошо сейчас. Они значительно оптимизировали инкрементные сборки в swift.

Я настоятельно рекомендую вам создать swift2.0 переходите и обновляйте свой код, используя бета-версию XCode 7. Вы будете довольны улучшениями компилятора (однако я бы сказал, что глобальное состояние XCode 7 все еще медленное и глючное)

ОБНОВЛЕНИЕ С Xcode 8.2

Прошло много времени с момента моего последнего обновления по этому вопросу, так что вот оно.

Наше приложение теперь содержит около 20 тыс. Строк почти исключительно кода Swift, что является достойным, но не выдающимся. Он прошел быструю миграцию 2 и 3. Компиляция MacBook Pro середины 2014 года (Intel Core i7 с тактовой частотой 2,5 ГГц, 2,5 ГГц) занимает около 5/6 м, что вполне приемлемо для чистой сборки.

Однако инкрементная сборка все еще шутка, несмотря на то, что Apple утверждает, что:

XCode не будет перестраивать всю цель, когда произошли только небольшие изменения. (28892475)

Очевидно, я думаю, что многие из нас просто смеялись после проверки этой чепухи (добавление одного частного (private!) Свойства в любой файл моего проекта перекомпилирует все это...)

Я хотел бы указать вам, ребята, на эту ветку на форумах разработчиков Apple, на которой есть еще немного информации об этой проблеме (а также высоко ценить общение Apple с разработчиками по этому вопросу время от времени)

В основном люди придумали несколько вещей, чтобы попытаться улучшить инкрементную сборку:

  1. Добавить HEADER_MAP_USES_VFS настройка проекта установлена ​​на true
  2. запрещать Find implicit dependencies по вашей схеме
  3. Создайте новый проект и переместите иерархию файлов в новый.

Я попробую решение 3, но решение 1/2 у нас не сработало.

Что забавно во всей этой ситуации, так это то, что, глядя на первый пост по этому вопросу, мы использовали Xcode 6 с кодом, как мне кажется, swift 1 или swift 1.1, когда достигли вялости первых компиляций, а теперь, спустя два года, несмотря на реальные улучшения Apple, Ситуация так же плоха, как это было с Xcode 6. Как иронично.

Я на самом деле ДЕЙСТВИТЕЛЬНО сожалею о том, что выбрал Swift вместо Obj/C для нашего проекта из-за ежедневных разочарований, которые он вызывает. (Я даже переключаюсь на AppCode, но это другая история)

В любом случае, я вижу, что это ТАК сообщение имеет 32k+ просмотров и 143 подъема на момент написания этой статьи, так что я думаю, что я не единственный. Держитесь, ребята, несмотря на пессимизм по поводу этой ситуации, в конце туннеля может быть немного света.

Если у вас есть время (и смелость!), Думаю, Apple приветствует радар по этому поводу.

До следующего раза! ура

ОБНОВЛЕНИЕ с Xcode 9

Наткнуться на это сегодня. Xcode незаметно представил новую систему сборки для улучшения текущей ужасной производительности. Вы должны включить его через настройки рабочего пространства.

введите описание изображения здесь

Уже попробовали, но обновите этот пост после того, как это будет сделано. Выглядит многообещающе, хотя.

21 ответ

Что ж, оказалось, что Роб Нейпир был прав. Это был один единственный файл (фактически один метод), который заставлял компилятор работать с ошибками.

Не поймите меня неправильно. Swift перекомпилирует все ваши файлы каждый раз, но самое замечательное то, что Apple добавила обратную связь компиляции в реальном времени по файлам, которые она компилирует, поэтому Xcode 6 GM теперь показывает, какие файлы Swift компилируются, и статус компиляции в режиме реального времени. как вы можете видеть на этом скриншоте:

Введите описание изображения здесь

Так что это очень удобно, чтобы узнать, какой из ваших файлов занимает так много времени. В моем случае это был кусок кода:

var dic = super.json().mutableCopy() as NSMutableDictionary
dic.addEntriesFromDictionary([
        "url" : self.url?.absoluteString ?? "",
        "title" : self.title ?? ""
        ])

return dic.copy() as NSDictionary

потому что собственность title был типа var title:String? и не NSString, Компилятор сходил с ума, добавляя его в NSMutableDictionary,

Меняя это на:

var dic = super.json().mutableCopy() as NSMutableDictionary
dic.addEntriesFromDictionary([
        "url" : self.url?.absoluteString ?? "",
        "title" : NSString(string: self.title ?? "")
        ])

return dic.copy() as NSDictionary

заставил компиляцию идти с 10/15 секунд (может быть, даже больше) до одной секунды... удивительно.

Мы попробовали немало вещей для борьбы с этим, поскольку у нас есть около 100 тыс. Строк кода Swift и 300 тыс. Строк кода ObjC.

Нашим первым шагом была оптимизация всех функций в соответствии с выводом времени компиляции функций (например, как описано здесь https://thatthinginswift.com/debug-long-compile-times-swift/)

Затем мы написали скрипт для объединения всех файлов swift в один файл, это нарушает уровни доступа, но это привело к увеличению времени компиляции с 5-6 минут до ~1 минуты.

Теперь это несущественно, потому что мы спросили об этом Apple, и они посоветовали нам сделать следующее:

  1. Включите "оптимизацию всего модуля" в настройке сборки "Swift Compiler - Code Generation". Выбрать 'Fast, Whole Module Optimization'

  1. В 'Swift Compiler - Custom Flags' для ваших разработок добавьте '-Onone'

Когда эти флаги установлены, компилятор скомпилирует все файлы Swift за один шаг. Мы обнаружили, что с помощью нашего скрипта слияния это намного быстрее, чем индивидуальная компиляция файлов Однако без-Onone' переопределить, это также оптимизирует весь модуль, что медленнее. Когда мы устанавливаем '-Onone' пометка в других флагах Swift, она останавливает оптимизацию, но не останавливает компиляцию всех файлов Swift за один шаг.

Для получения дополнительной информации об оптимизации всего модуля, проверьте сообщение в блоге Apple здесь - https://swift.org/blog/whole-module-optimizations/

Мы обнаружили, что эти настройки позволяют скомпилировать наш код Swift за 30 секунд:-) У меня нет никаких доказательств того, как он будет работать в других проектах, но я предлагаю попробовать, если время компиляции Swift все еще остается для вас проблемой.

Примечание для сборки вашего магазина приложений, вы должны оставить '-Onone' отключить, так как оптимизация рекомендуется для производственных сборок.

Вероятно, это имеет мало общего с размером вашего проекта. Это, вероятно, какой-то конкретный фрагмент кода, возможно, даже одна строка. Вы можете проверить это, пытаясь скомпилировать один файл за раз, а не весь проект. Или попробуйте просмотреть журналы сборки, чтобы увидеть, какой файл занимает так много времени.

В качестве примера типов кода, которые могут вызвать проблемы, эта 38-строчная суть занимает более минуты для компиляции в бета-версии 7. Все это вызвано этим одним блоком:

let pipeResult =
seq |> filter~~ { $0 % 2 == 0 }
  |> sorted~~ { $1 < $0 }
  |> map~~ { $0.description }
  |> joinedWithCommas

Упростите это всего на одну или две строки, и он будет компилироваться практически мгновенно. Проблема в том, что это вызывает экспоненциальный рост (возможно, факторный рост) компилятора. Очевидно, что это не идеально, и если вы можете изолировать такие ситуации, вы должны открыть радары, чтобы помочь устранить эти проблемы.

Если вы пытаетесь определить конкретные файлы, которые замедляют ваше время компиляции, вы можете попробовать скомпилировать его из командной строки с помощью xctool, который даст вам время компиляции файл за файлом.

Следует отметить, что по умолчанию он создает 2 файла одновременно для каждого ядра ЦП и не даст вам "чистого" истекшего времени, а будет абсолютным "пользовательским" временем. Таким образом, все моменты времени распределяются между распараллеленными файлами и выглядят очень похоже

Чтобы преодолеть это, установите -jobs Отметьте 1, чтобы не распараллеливать сборки файлов. Это займет больше времени, но в итоге у вас будет "чистое" время компиляции, которое вы можете сравнить файл за файлом.

Это пример команды, которая должна сделать свое дело:

xctool -workspace <your_workspace> -scheme <your_scheme> -jobs 1 build

Вывод фазы "Compile Swift files" будет выглядеть примерно так:

...
   ✓ Compile EntityObserver.swift (1623 ms)
   ✓ Compile Session.swift (1526 ms)
   ✓ Compile SearchComposer.swift (1556 ms)
...

Из этого вывода вы можете быстро определить, какие файлы занимают больше времени, чем другие, для компиляции. Более того, вы можете с высокой точностью определить, уменьшают ли ваши рефакторинги (явное приведение, подсказки типов и т. Д.) Время компиляции для определенных файлов или нет.

ПРИМЕЧАНИЕ: технически вы также можете сделать это с xcodebuild но результат невероятно многословен и труден для потребления.

В моем случае Xcode 7 вообще ничего не изменил. У меня было несколько функций, требующих несколько секунд для компиляции.

пример

// Build time: 5238.3ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)

После развертывания опций время сборки сократилось на 99,4%.

// Build time: 32.4ms
var padding: CGFloat = 22
if let rightView = rightView {
    padding += rightView.bounds.width
}

if let leftView = leftView {
    padding += leftView.bounds.width
}
return CGSizeMake(size.width + padding, bounds.height)

Смотрите больше примеров в этом посте и этом посте.

Анализатор времени сборки для Xcode

Я разработал плагин Xcode, который может пригодиться всем, кто сталкивается с такими проблемами.

образ

Похоже, что в Swift 3 появятся улучшения, так что, надеюсь, мы увидим, что наш код Swift скомпилируется быстрее.

Возможно, мы не можем исправить компилятор Swift, но мы можем исправить наш код!

В компиляторе Swift есть скрытая опция, которая распечатывает точные интервалы времени, которые компилятор использует для компиляции каждой отдельной функции: -Xfrontend -debug-time-function-bodies, Это позволяет нам находить узкие места в нашем коде и значительно сокращать время компиляции.

Просто запустите следующее в терминале и проанализируйте результаты:

xcodebuild -workspace App.xcworkspace -scheme App clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep [1-9].[0-9]ms | sort -nr > culprits.txt

Обалденный Брайан Айрас написал блестящую статью об этом, " Профилируя время компиляции Swift".

Решение отливка.

У меня было огромное множество тонн словарей, как это:

["title" : "someTitle", "textFile" : "someTextFile"],
["title" : "someTitle", "textFile" : "someTextFile"],
["title" : "someTitle", "textFile" : "someTextFile"],
["title" : "someTitle", "textFile" : "someTextFile"],
.....

На его компиляцию ушло около 40 минут. Пока я не произнес словари вот так:

["title" : "someTitle", "textFile" : "someTextFile"] as [String : String],
["title" : "someTitle", "textFile" : "someTextFile"] as [String : String],
["title" : "someTitle", "textFile" : "someTextFile"] as [String : String],
....

Это работало практически для любой проблемы, с которой я столкнулся в отношении типов данных, которые я жестко запрограммировал в своем приложении.

Следует отметить, что механизм вывода типов Swift может быть очень медленным с вложенными типами. Вы можете получить общее представление о том, что вызывает медлительность, просматривая журнал сборки для отдельных модулей компиляции, которые занимают много времени, а затем копируют и вставляют полную порожденную Xcode команду в окно терминала, а затем нажимают CTRL-\, чтобы получить немного диагностики. Взгляните на http://blog.impathic.com/post/99647568844/debugging-slow-swift-compile-times для полного примера.

Также убедитесь, что при компиляции для отладки (либо Swift, либо Objective-C) вы установили только Build Active Architecture:

Поскольку весь этот материал находится в бета-версии, а компилятор Swift (по крайней мере, на сегодняшний день) не открыт, я думаю, что нет реального ответа на ваш вопрос.

Прежде всего, сравнение Objective-C с компилятором Swift несколько жестоко. Swift все еще находится в бета-версии, и я уверен, что Apple работает над обеспечением функциональности и исправлением ошибок, а не над молниеносной скоростью (вы не начинаете строить дом, покупая мебель). Я думаю, что Apple будет оптимизировать компилятор в свое время.

Если по какой-либо причине все исходные файлы должны быть скомпилированы полностью, можно создать отдельные модули / библиотеки. Но этот вариант пока невозможен, так как Swift не может разрешать библиотеки до тех пор, пока язык не станет стабильным.

Я предполагаю, что они будут оптимизировать компилятор. По той же причине, по которой мы не можем создавать предварительно скомпилированные модули, вполне может быть, что компилятору нужно скомпилировать все с нуля. Но как только язык достигнет стабильной версии и формат двоичных файлов больше не меняется, мы сможем создавать наши библиотеки, и, возможно, (?) Компилятор также сможет оптимизировать свою работу.

Хотя, думаю, только Apple знает...

Для Xcode 8 перейдите в настройки проекта, затем "Редактор"> "Добавить настройку сборки"> "Добавить пользовательскую настройку" и добавьте следующее:

SWIFT_WHOLE_MODULE_OPTIMIZATION = YES

Добавление этого флага сократило время компиляции чистой сборки с 7 минут до 65 секунд для проекта 40KLOC swift, чудесным образом. Также могу подтвердить, что 2 друга видели похожие улучшения в корпоративных проектах.

Я могу только предположить, что это какая-то ошибка в Xcode 8.0

РЕДАКТИРОВАТЬ: Кажется, он не работает больше в Xcode 8.3 для некоторых людей.

К сожалению, компилятор Swift все еще не оптимизирован для быстрой и пошаговой компиляции (начиная с бета-версии Xcode 6.3). Тем временем вы можете использовать некоторые из следующих методов для улучшения времени компиляции Swift:

  • Разделите приложение на Frameworks, чтобы уменьшить влияние перекомпиляции. Но имейте в виду, что вы должны избегать циклических зависимостей в вашем приложении. Для получения дополнительной информации по этой теме проверьте этот пост: http://bits.citrusbyte.com/improving-swift-compile-time/

  • Используйте Swift для тех частей вашего проекта, которые достаточно стабильны и не меняются часто. Для других областей, где вам нужно часто менять, или областей, требующих выполнения большого количества итераций компиляции / запуска (почти любой связанный с пользовательским интерфейсом материал), лучше использовать Objective-C с подходом смешивания и сопоставления.

  • Попробуйте внедрение кода во время выполнения с помощью "Инъекции для Xcode"

  • Используйте метод roopc: http://roopc.net/posts/2014/speeding-up-swift-builds/

  • Облегчите механизм быстрого вывода типа, дав несколько подсказок с явными приведениями.

Конструкция быстрых массивов и словарей, кажется, довольно популярная причина для этого (особенно для тех, кто имеет опыт работы с Ruby), то есть

var a = ["a": "b",
         "c": "d",
         "e": "f",
         "g": "h",
         "i": "j",
         "k": "l",
         "m": "n",
         "o": "p",
         "q": "r",
         "s": "t",
         "u": "v",
         "x": "z"]

вероятно, будет причиной, по которой это следует исправить:

var a = NSMutableDictionary()
a["a"] = "b"
a["c"] = "d"
... and so on

Для проектов, которые смешивают код Objective-C и Swift, мы можем установить -enable-bridging-pch в Other Swift Flags, При этом заголовок моста анализируется только один раз, и результат (временный "предварительно скомпилированный заголовок" или файл "PCH") кэшируется и повторно используется во всех файлах Swift в целевом объекте. Apple утверждает, что это уменьшает время сборки на 30%. Ссылка Ссылка:

ПРИМЕЧАНИЕ. Это работает только для Swift 3.1 и выше.

Для отладки и тестирования обязательно используйте следующие параметры, чтобы сократить время компиляции с примерно 20 минут до менее 2 минут,

  1. В настройках сборки проекта выполните поиск по запросу "Оптимизация". Установите параметр "Отладка" на "Самый быстрый [-O3]" или выше.
  2. Установить сборку для активной архитектуры: ДА
  3. Формат отладочной информации: DWARF
  4. Оптимизация всего модуля: НЕТ

Я потратил бесчисленные часы, ожидая, пока проект будет построен, только чтобы понять, что мне нужно было внести одно небольшое изменение, и мне пришлось ждать еще целых 30 минут, чтобы протестировать его. Это настройки, которые работали для меня. (Я все еще экспериментирую с настройками)

Но убедитесь, что вы, по крайней мере, установили "DWARF с dSYM" (если вы хотите отслеживать свое приложение) и "Построить активную архитектуру" на "НЕТ" для выпуска / архивации, чтобы отправить его в iTunes Connect (я тоже помню, что здесь я тоже потратил несколько часов).

Компилятор тратит много времени на вывод и проверку типов. Поэтому добавление аннотаций типов очень помогает компилятору.

Если у вас есть много вызовов функций, таких как

let sum = [1,2,3].map({String($0)}).flatMap({Float($0)}).reduce(0, combine: +)

Затем компилятору требуется время, чтобы выяснить, какой тип sum должно быть. Добавление типа помогает. То, что также помогает, тянет прерывистые шаги в отдельные переменные.

let numbers: [Int] = [1,2,3]
let strings: [String] = sum.map({String($0)})
let floats: [Float] = strings.flatMap({Float($0)})
let sum: Float = floats.reduce(0, combine: +)

Специально для числовых типов CGFloat, Int это может очень помочь. Буквенное число как 2 может представлять множество различных числовых типов. Таким образом, компилятор должен выяснить из контекста, какой он есть.

Функции, которые требуют много времени, чтобы выглядеть как + также следует избегать. Используя несколько + объединять несколько массивов медленно, потому что компилятор должен выяснить, какая реализация + должен быть вызван для каждого +, Так что используйте var a: [Foo] с append() вместо этого, если это возможно.

Вы можете добавить предупреждение, чтобы определить, какие функции медленно компилируются в XCode.

В настройках сборки для вашей цели найдите другие флаги Swift и добавьте

-Xfrontend -warn-long-function-bodies=100

предупредить для каждой функции, которая компилируется дольше 100 мс.

Перезагрузка моего Mac сделала чудеса для этой проблемы. Я перешел с 15-минутных сборок на 30-секундные, просто перезагрузившись.

Смешивание целочисленного литерала и литерала с плавающей запятой в одном выражении также приводит к длительному времени компиляции.

1.0 + (1.0 + (1  * (1.0 + 1.0))) // 3429ms

1.0 + (1.0 + (1.0  * (1.0 + 1.0))) // 5ms

Многие выражения времени компиляции 1000+ мс сокращаются до 10~100 мс после того, как я помещаю .0 после целочисленного литерала.

Быстрое время компиляции было улучшено в новом Xcode 6.3

Улучшения компилятора

Компилятор Swift 1.2 был спроектирован так, чтобы быть более стабильным и повышать производительность во всех отношениях. Эти изменения также обеспечивают лучший опыт при работе с Swift в Xcode. Некоторые из наиболее заметных улучшений включают в себя:

Инкрементные сборки

Исходные файлы, которые не были изменены, больше не будут перекомпилироваться по умолчанию, что значительно улучшит время сборки в большинстве распространенных случаев. Для более крупных структурных изменений в вашем коде может потребоваться перестройка нескольких файлов.

Быстрее исполняемые файлы

Отладочные сборки производят двоичные файлы, которые работают значительно быстрее, а новые оптимизации обеспечивают еще лучшую производительность сборки выпуска.

Лучшая диагностика компилятора

Более четкие сообщения об ошибках и предупреждения вместе с новым Fix-it облегчают написание правильного кода Swift 1.2.

Улучшения стабильности

Наиболее распространенные ошибки компилятора были исправлены. Вы также должны видеть меньше предупреждений SourceKit в редакторе Xcode.

Вот еще один случай, который может вызвать значительное замедление с выводом типа. Объединение операторов.

Изменение строк, как:

abs(some_optional_variable ?? 0)

в

abs((some_optional_variable ?? 0) as VARIABLE_TYPE)

помог перенести время компиляции с 70 на 13

В Xcode 6.3.1 у меня ничего не получалось - когда я добавил около 100 файлов Swift, Xcode случайно зависал при сборке и / или индексации. Я попробовал модульный вариант без успеха.

Установка и использование Xcode 6.4 Beta фактически работали для меня.

Для меня это работало как волшебство - Speed ​​Up Swift Compilation. Это уменьшило время компиляции до 3 минут с 10 минут.

Он говорит, что вы должны включить Whole Module Optimization при добавлении -Onone в Other Swift Flags,

я использую Swift 3 на Xcode 8.3 / Xcode 8.2,

Другие вопросы по тегам