compilation - for - Warum ist die Swift-Kompilierzeit so langsam?



(14)

Wahrscheinlich können wir den Swift-Compiler nicht reparieren, aber etwas, das wir reparieren können, ist unser Code!

Es gibt eine versteckte Option im Swift-Compiler, die die exakten Zeitintervalle -Xfrontend -debug-time-function-bodies , die der Compiler benötigt, um jede einzelne Funktion zu kompilieren: -Xfrontend -debug-time-function-bodies . Es ermöglicht uns, Engpässe in unserem Code zu finden und die Kompilierzeit signifikant zu verbessern.

Einfach im Terminal ausführen und Ergebnisse analysieren:

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

Awesome Brian Irace hat einen brillianten Artikel darüber geschrieben.

Ich benutze Xcode 6 Beta 6.

Das ist etwas, das mich schon seit einiger Zeit nervt, aber es erreicht einen Punkt, an dem es jetzt kaum noch verwendbar ist.

Mein Projekt hat eine anständige Größe von 65 Swift-Dateien und einigen überbrückten Objective-C-Dateien (die wirklich nicht die Ursache des Problems sind).

Es scheint, als würde jede geringfügige Änderung an einer Swift-Datei (wie das Hinzufügen eines einfachen Leerzeichens in einer Klasse, die in der App kaum verwendet wird) dazu führen, dass die gesamten Swift-Dateien für das angegebene Ziel neu kompiliert werden.

Nach einer tieferen Untersuchung habe ich festgestellt, dass die CompileSwift Phase, in der Xcode den swiftc Befehl für alle Swift-Dateien des Ziels CompileSwift , in etwa 100% der Compilerzeit CompileSwift .

Ich habe weitere Nachforschungen angestellt, und wenn ich den App-Delegaten nur mit einem Standard-Controller behalte, ist die Kompilierung sehr schnell, aber als ich immer mehr meiner Projektdateien hinzufüge, begann die Kompilierzeit wirklich langsam zu werden.

Jetzt mit nur 65 Quelldateien dauert es etwa 8/10 Sekunden, um jedes Mal zu kompilieren. Nicht sehr schnell überhaupt.

Ich habe bis jetzt keinen Beitrag über dieses Problem gesehen, aber es war eine alte Version von Xcode 6. Ich frage mich, ob ich der einzige in diesem Fall bin.

AKTUALISIEREN

Ich habe ein paar Swift-Projekte auf GitHub wie Alamofire , Euler und CryptoSwift , aber keiner von ihnen hatte genug Swift-Dateien, um sie wirklich zu vergleichen. Das einzige Projekt, das ich fand, war SwiftHN , und obwohl es nur ein Dutzend Quelldateien hatte, war ich immer noch in der Lage, dasselbe zu überprüfen, einen einfachen Platz und das gesamte Projekt musste neu kompiliert werden kleine Zeit (2/3 Sekunden).

Im Vergleich zu Objective-C-Code, wo Analyzer und Compilation blitzschnell sind, fühlt es sich an, als ob Swift niemals in der Lage sein wird, große Projekte zu bewältigen, aber bitte sag mir, dass ich falsch liege.

UPDATE mit Xcode 6 Beta 7

Immer noch keine Verbesserung. Das wird langsam lächerlich. Mit dem Mangel an #import in Swift sehe ich wirklich nicht, wie Apple jemals in der Lage sein wird, dies zu optimieren.

UPDATE Mit Xcode 6.3 und Swift 1.2

Apple hat inkrementelle Builds (und viele andere Compileroptimierungen) hinzugefügt. Sie müssen Ihren Code zu Swift 1.2 migrieren, um diese Vorteile zu sehen, aber Apple hat ein Tool in Xcode 6.3 hinzugefügt, um Ihnen dabei zu helfen:

JEDOCH

Freue dich nicht zu schnell wie ich. Der Graph-Solver, den sie verwenden, um den Build inkrementell zu erstellen, ist noch nicht sehr gut optimiert.

In der Tat sieht es zunächst nicht nach Änderungen der Funktionssignatur. Wenn Sie also im Block einer Methode ein Leerzeichen hinzufügen, werden alle von dieser Klasse abhängigen Dateien neu kompiliert.

Zweitens scheint es, dass die Baumstruktur basierend auf den Dateien erstellt wurde, die neu kompiliert wurden, selbst wenn eine Änderung sie nicht beeinflusst. Zum Beispiel, wenn Sie diese drei Klassen in verschiedene Dateien verschieben

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

Wenn Sie nun FileA ändern, wird der Compiler offensichtlich FileA für die Neukompilierung markieren. Es wird auch FileB neu FileB (das wäre OK, basierend auf den Änderungen von FileA ), aber auch FileC weil FileB neu kompiliert wird, und das ist ziemlich schlecht, weil FileC niemals FileA benutzt.

Ich hoffe also, dass sie den Abhängigkeitsbaumlöser verbessern ... Ich habe ein radar mit diesem Beispielcode geöffnet.

UPDATE Mit Xcode 7 Beta 5 und Swift 2.0

Gestern hat Apple die Beta 5 veröffentlicht und in den Release Notes konnten wir sehen:

Swift Language & Compiler • Inkrementelle Builds: Wenn nur der Rumpf einer Funktion geändert wird, sollten abhängige Dateien nicht mehr neu erstellt werden. (15352929)

Ich habe es versucht und ich muss sagen, es funktioniert wirklich (wirklich!) Gut jetzt. Sie haben die inkrementellen Builds in Swift stark optimiert.

Ich empfehle Ihnen, einen swift2.0 Zweig zu erstellen und Ihren Code mit XCode 7 Beta 5 auf dem neuesten Stand zu halten. Sie werden von den Verbesserungen des Compilers erfreut sein (allerdings würde ich sagen, dass der globale Zustand von XCode 7 immer noch langsam und fehlerhaft ist )

UPDATE mit Xcode 8.2

Es ist eine Weile her seit meinem letzten Update zu diesem Thema, also hier ist es.

Unsere App ist jetzt etwa 20k Zeilen fast ausschließlich Swift-Code, die anständig ist, aber nicht überragend. Es wurde schnell 2 und dann schnell 3 Migration. Es dauert etwa 5 / 6m, um auf einem Mitte 2014 Macbook Pro (2,5 GHz Intel Core i7) zu kompilieren, was auf einem sauberen Build in Ordnung ist.

Allerdings ist der inkrementelle Build immer noch ein Witz, obwohl Apple behauptet, dass:

Xcode wird ein gesamtes Ziel nicht neu erstellen, wenn nur kleine Änderungen aufgetreten sind. (28892475)

Offensichtlich denke ich, dass viele von uns nur gelacht haben, nachdem sie diesen Unsinn überprüft haben (das Hinzufügen einer privaten (privaten!) Eigenschaft zu jeder Datei meines Projekts wird die ganze Sache neu kompilieren ...)

Ich möchte euch auf diesen Thread in den Entwicklerforen von Apple hinweisen, der einige weitere Informationen zu dem Problem enthält (und auch die Apple-Entwickler-Kommunikation zu dieser Angelegenheit von Zeit zu Zeit geschätzt haben).

Im Grunde haben sich die Leute ein paar Dinge ausgedacht, um den inkrementellen Build zu verbessern:

  1. Fügen Sie eine HEADER_MAP_USES_VFS Projekteinstellung auf true
  2. Deaktivieren Find implicit dependencies von Ihrem Schema
  3. Erstellen Sie ein neues Projekt und verschieben Sie Ihre Dateihierarchie auf die neue.

Ich werde Lösung 3 versuchen, aber Lösung 1/2 hat nicht für uns funktioniert.

Was ironisch witzig ist in dieser ganzen Situation ist, dass wir beim ersten Post zu diesem Thema Xcode 6 mit dem Code swift 1 oder swift 1.1 verwendeten, als wir die erste Kompilation Schlappheit erreichten und nun etwa zwei Jahre später trotz der tatsächlichen Verbesserungen von Apple die Die Situation ist genauso schlecht wie bei Xcode 6. Wie ironisch.

Ich bedauere wirklich, Swift über Obj / C für unser Projekt zu wählen, wegen der täglichen Frustration. (Ich wechsle sogar zu AppCode, aber das ist eine andere Geschichte)

Jedenfalls sehe ich, dass dieser SO Post 32k + Views und 143 Ups hat, so dass ich denke, dass ich nicht der Einzige bin. Bleib dran, Jungs, obwohl es pessimistisch ist, könnte es am Ende des Tunnels Licht geben.

Wenn Sie die Zeit (und Mut!) Haben, ich denke, Apple begrüßt Radar darüber.

Bis zum nächsten Mal! Prost

UPDATE mit Xcode 9

Stolpern Sie heute darüber. Xcode führte leise ein neues Build-System ein, um die derzeitige schreckliche Leistung zu verbessern. Sie müssen es über die Einstellungen des Arbeitsbereichs aktivieren.

Ich habe es schon einmal versucht, werde aber diesen Beitrag aktualisieren, nachdem es fertig ist. Sieht vielversprechend aus.


Answer #1

Da all diese Sachen in Beta sind und der Swift-Compiler (zumindest ab heute) nicht geöffnet ist, denke ich, dass es keine echte Antwort auf deine Frage gibt.

Zuallererst ist der Vergleich von Objective-C mit Swift Compiler irgendwie grausam. Swift ist immer noch in der Beta, und ich bin mir sicher, dass Apple daran arbeitet, Funktionalität bereitzustellen und Bugs zu beheben, mehr als blitzschnell zu sein (Sie beginnen nicht, ein Haus zu bauen, indem Sie Möbel kaufen). Ich denke, Apple wird den Compiler rechtzeitig optimieren.

Wenn aus irgendeinem Grund alle Quelldateien kompiliert werden müssen, könnte es eine Option sein, getrennte Module / Bibliotheken zu erstellen. Aber diese Option ist noch nicht möglich, da Swift Bibliotheken erst erlauben kann, wenn die Sprache stabil ist.

Meine Vermutung ist, dass sie den Compiler optimieren werden. Aus dem gleichen Grund, dass wir keine vorkompilierten Module erstellen können, kann es durchaus sein, dass der Compiler alles von Grund auf kompiliert. Aber sobald die Sprache eine stabile Version erreicht hat und sich das Format der Binärdateien nicht mehr ändert, werden wir in der Lage sein, unsere Bibliotheken zu erstellen, und vielleicht (?) Wird der Compiler auch in der Lage sein, seine Arbeit zu optimieren.

Nur raten, nur für Apple weiß ...


Answer #2

Der Compiler verbringt viel Zeit damit, die Typen abzuleiten und zu prüfen. Das Hinzufügen von Typ-Annotationen hilft dem Compiler sehr.

Wenn Sie viele verkettete Funktionsaufrufe haben wie

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

Dann braucht der Compiler eine Weile, um herauszufinden, wie die Art der sum sein soll. Hinzufügen des Typs hilft. Es hilft auch, die intermittierenden Schritte in separate Variablen zu ziehen.

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: +)

Speziell für numerische Typen CGFloat , Int kann es sehr hilfreich sein. Eine wörtliche Zahl wie 2 kann viele verschiedene numerische Typen darstellen. Der Compiler muss also aus dem Kontext herausfinden, um welchen es sich handelt.

Funktionen, die viel Zeit zum Nachschlagen + sollten ebenfalls vermieden werden. Die Verwendung mehrerer + zum Verketten mehrerer Arrays ist langsam, da der Compiler herausfinden muss, welche Implementierung von + für jedes + aufgerufen werden soll. Verwenden Sie stattdessen var a: [Foo] mit append() wenn möglich.

Sie können eine Warnung hinzufügen, um festzustellen, welche Funktionen in Xcode nur langsam kompiliert werden .

In Build-Einstellungen für Ihre Zielsuche nach anderen Swift-Flags und hinzufügen

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

für jede Funktion warnen, die länger als 100 ms zum Kompilieren benötigt.


Answer #3

Die Lösung ist Gießen.

Ich hatte eine riesige Menge von Tonnen von Wörterbüchern, so:

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

Es dauerte ungefähr 40 Minuten, um es zu kompilieren. Bis ich die Wörterbücher so gegossen habe:

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

Dies funktionierte bei fast allen anderen Problemen, die ich in Bezug auf Datentypen hatte, die ich fest in meine Anwendung einprogrammierte.


Answer #4

Eine Sache zu beachten ist, dass die Swift-Typ-Inferenz-Engine mit verschachtelten Typen sehr langsam sein kann. Sie können eine allgemeine Vorstellung davon bekommen, was die Langsamkeit verursacht, indem Sie das Erstellungsprotokoll für einzelne Kompilierungseinheiten betrachten, die eine lange Zeit benötigen, und dann den vollständigen Xcode-erzeugten Befehl in ein Terminalfenster kopieren und einfügen und dann STRG- \ drücken einige Diagnosen. Werfen Sie einen Blick auf http://blog.impathic.com/post/99647568844/debugging-slow-swift-compile-times für ein vollständiges Beispiel.


Answer #5

Es hat wahrscheinlich wenig mit der Größe Ihres Projekts zu tun. Es ist wahrscheinlich ein bestimmter Code, möglicherweise sogar nur eine Zeile. Sie können dies testen, indem Sie versuchen, eine Datei gleichzeitig zu kompilieren, anstatt das gesamte Projekt. Oder sehen Sie sich die Build-Protokolle an, um zu sehen, welche Datei so lange dauert.

Als ein Beispiel für die Arten von Code, die Probleme verursachen können, dauert dieser 38-Zeilen-Text mehr als eine Minute, um in Beta7 zu kompilieren. All das wird durch diesen einen Block verursacht:

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

Vereinfachen Sie das um eine oder zwei Zeilen, und es wird fast sofort kompiliert. Das Problem ist, dass es im Compiler zu exponentiellem Wachstum (möglicherweise faktoriellem Wachstum) kommt. Offensichtlich ist das nicht ideal, und wenn Sie solche Situationen isolieren können, sollten Sie Radare öffnen, um diese Probleme zu beheben.


Answer #6

Hier ist ein weiterer Fall, der zu massiven Verlangsamungen mit Typinferenz führen kann. Zusammenwachsende Betreiber .

Zeilen ändern wie:

abs(some_optional_variable ?? 0)

zu

abs((some_optional_variable ?? 0) as VARIABLE_TYPE)

half mir meine Kompilierzeit von 70s auf 13s zu bringen


Answer #7

In meinem Fall hat Xcode 7 überhaupt keinen Unterschied gemacht. Ich hatte mehrere Funktionen, die mehrere Sekunden benötigten, um zu kompilieren.

Beispiel

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

Nach dem Auspacken der Optionals fiel die Buildzeit um 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)

Weitere Beispiele finden Sie in diesem Post und diesem Post .

Build Time Analyzer für Xcode

Ich habe ein Xcode-Plug-in entwickelt, das für jeden nützlich sein kann, der diese Probleme hat.

Es scheint Verbesserungen in Swift 3 zu geben, also werden wir hoffentlich sehen, dass unser Swift-Code schneller kompiliert wird.


Answer #8

Neustart meines Mac hat Wunder für dieses Problem. Ich ging von 15 Minuten Builds zu 30 Sekunden Builds nur durch einen Neustart.


Answer #9

Nichts funktionierte für mich in Xcode 6.3.1 - als ich rund 100 Swift-Dateien hinzufügte, wurde Xcode willkürlich an Build und / oder Indizierung gehängt. Ich habe eine modulare Option ohne Erfolg ausprobiert.

Die Installation und Verwendung von Xcode 6.4 Beta funktionierte tatsächlich für mich.


Answer #10

Stellen Sie außerdem sicher, dass Sie beim Kompilieren für Debug (entweder Swift oder Objective-C) auf Nur aktive Architektur erstellen setzen:


Answer #11

Stellen Sie zum Debuggen und Testen sicher, dass Sie die folgenden Einstellungen verwenden, um die Kompilierzeit von etwa 20 Minuten auf weniger als 2 Minuten zu reduzieren.

  1. Suchen Sie in den Projektbuild-Einstellungen nach "Optimization". Schalten Sie Debug auf "Am schnellsten [-O3]" oder höher.
  2. Setze Build für aktive Architektur: JA
  3. Debug-Informationsformat: DWARF
  4. Optimierung ganzer Module: NEIN

Ich habe unzählige Stunden damit verbracht, auf das Projekt zu warten, nur um zu realisieren, dass ich diese kleine Veränderung machen musste und musste noch 30 Minuten warten, um es zu testen. Dies sind die Einstellungen, die für mich funktionierten. (Ich experimentiere immer noch mit den Einstellungen)

Stellen Sie jedoch sicher, dass Sie mindestens "DWARF mit dSYM" (wenn Sie Ihre Anwendung überwachen möchten) und "Build Active Architecture" auf "NEIN" für Release / Archiving setzen, um zu iTunes Connect zu wechseln (ich erinnere mich, dass ich auch hier ein paar Stunden verschwendet habe).


Answer #12

Wechseln Sie für Xcode 8 zu den Projekteinstellungen, dann zu Editor> Build-Einstellungen hinzufügen> Benutzerdefinierte Einstellungen hinzufügen und fügen Sie Folgendes hinzu:

SWIFT_WHOLE_MODULE_OPTIMIZATION = YES

Das Hinzufügen dieses Flags hat unsere Clean-Build-Kompilierungszeiten von 7 Minuten auf 65 Sekunden für ein 40KLOC-Swift-Projekt reduziert, wie durch ein Wunder. Kann auch bestätigen, dass 2 Freunde ähnliche Verbesserungen bei Enterprise-Projekten gesehen haben.

Ich kann nur annehmen, dass dies ein Fehler in Xcode 8.0 ist

EDIT: Es scheint nicht mehr in Xcode 8.3 für einige Leute zu arbeiten.


Answer #13

Wenn Sie versuchen, bestimmte Dateien zu identifizieren, die die Kompilierungszeit verlangsamen, können Sie versuchen, sie über die Befehlszeile über xctool kompilieren, wodurch Sie die Kompilierungszeiten Datei für Datei erhalten.

Zu beachten ist, dass standardmäßig 2 Dateien gleichzeitig pro CPU-Kern erstellt werden und Ihnen nicht die verstrichene "Nettozeit", sondern die absolute "Benutzerzeit" angezeigt wird. Auf diese Weise gleichen sich alle Timings zwischen parallelisierten Dateien aus und sehen sehr ähnlich aus.

Um dies zu -jobs , setzen Sie das -jobs Flag auf 1 , so dass es Datei-Builds nicht parallelisiert. Es dauert länger, aber am Ende haben Sie "net" Kompilierzeiten, die Sie Datei für Datei vergleichen können.

Dies ist ein Beispielbefehl, der den Trick machen sollte:

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

Die Ausgabe der "Compile Swift files" -Phase würde etwa lauten:

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

Anhand dieser Ausgabe können Sie schnell feststellen, welche Dateien länger dauern als andere, um sie zu kompilieren. Außerdem können Sie mit hoher Genauigkeit feststellen, ob Ihre Refactorings (explizite Umwandlungen, Typhinweise usw.) die Kompilierungszeiten für bestimmte Dateien verringern oder nicht.

Hinweis: technisch können Sie es auch mit xcodebuild tun, aber die Ausgabe ist unglaublich ausführlich und schwer zu konsumieren.