Introduction : Le compilateur Swift
Si vous êtes un développeur iOS, il est fort probable que vous ayez constaté que Swift est rapidement devenu l’un des langages de programmation natifs les plus populaires ces dernières années, grâce à sa syntaxe moderne, ses fonctionnalités de sécurité et ses performances.
Que vous soyez en train de déboguer une petite fonctionnalité ou d’optimiser une application en production, choisir le bon mode de compilation peut vous faire gagner des heures de temps de développement. Dans cet article, nous allons explorer en profondeur le compilateur Swift, en examinant ses composants clés, leur interaction et les facteurs qui influencent ses performances. À la fin, vous aurez une compréhension claire du processus de compilation, vous permettant d’écrire un code plus efficace et d’optimiser vos builds.

Pourquoi le temps de build augmente avec plus de modules
Si vous travaillez sur un projet avec plusieurs fonctionnalités, et que chacune étant son propre module, le temps de build et les performances sont probablement des préoccupations quotidiennes. Au début de mon investigation a ce sujet j’ai remarqué que plus vous ajoutez de modules à votre application, plus le temps de build est long. Mais pourquoi ?

Pour comprendre cela, revenons à la manière dont le code Swift est compilé. Que vous construisiez un petit script ou une application à grande échelle, vous utiliserez soit swift, soit swiftc :

  • swift : L’interpréteur Swift. Il exécute directement les scripts (par exemple, swift script.swift) sans les compiler en binaire. C’est pourquoi Xcode peut instantanément afficher les relations entre les méthodes et les variables sans avoir à construire ou exécuter votre application. Il interprète le code à la volée.
  • swiftc : (The Driver) Le pilote du compilateur Swift . Il orchestre l’ensemble du processus de compilation, transformant le code source Swift en binaires exécutables par la machine. Lorsque vous construisez un projet, swiftc gère des sous-processus tels que la compilation, le linking et l’attachement des symboles de déboggage. Par exemple, lors d’un build dans Xcode, vous verrez des messages comme “Linking…” cela c’est une étape parmi les étapes que swiftc est sensé faire.

Le Driver : L’orchestrateur du processus de compilation


Le Driver est le processus de plus haut niveau dans le pipeline de compilation de swiftc. Imaginez-le comme le chef d’orchestre : il ne réalise pas lui-même la compilation, mais il coordonne tous les sous-processus nécessaires pour transformer votre code en un exécutable.

Rôles du Driver :

  • Il analyse les arguments passés en ligne de commande.
  • Il détermine la séquence des jobs (tâches) nécessaires à la compilation.
  • Il invoque des sous-processus tels que le frontend, le linker et les outils de débogage.
  • Il gère les dépendances et garantit que les opérations s’exécutent dans le bon ordre.

Les Jobs dans le pipeline de compilation


Le Driver lance plusieurs jobs, parmi lesquels le Frontend Job est le plus critique. Lorsque Xcode exécute swift -frontend, le Driver initie des jobs frontend—la première étape de la compilation après que votre code a été converti en une représentation intermédiaire.

Tâches au sein des Frontend Jobs :

  1. Parsing (Analyse syntaxique) : Convertit le code Swift en un arbre syntaxique abstrait (Abstract Syntax Tree, AST).
  2. Type Checking (Vérification des types) : Assure la correction sémantique (par exemple, vérifie que les types des variables correspondent à leur utilisation).
  3. Génération de SIL : Produit le Swift Intermediate Language (SIL), une représentation optimisée de votre code.
  4. Optimisation : Applique des optimisations de haut niveau au SIL (par exemple, la suppression de code mort).
  5. Génération de code : Convertit le SIL en LLVM Intermediate Representation (IR), qui est ensuite transmis au backend LLVM pour la génération de code machine.

Le frontend est l’endroit où la plupart des optimisations spécifiques à Swift ont lieu, garantissant que votre code est à la fois efficace et exempt d’erreurs.

Autres Jobs clés :

  • ld : Le linker combine les fichiers objets et les bibliothèques en un seul exécutable.
  • swift -modulewrap : Encapsule les modules Swift pour une réutilisation dans d’autres projets.
  • swift-autolink-extract : Extrait les informations d’autolink pour les bibliothèques.
  • dsymutil : Génère les symboles de débogage pour des outils comme le débogueur de Xcode.
  • dwarfdump : Analyse les informations de débogage dans les binaires.
+-------------------+
|      Driver       |  (Orchestrates the process)
+-------------------+
          |
          v
+-------------------+
|   Frontend Jobs   |   (Parsing, Type Checking, SIL)
+-------------------+    (Generation, Optimization)
          |
          v
+-------------------+
|    Other Jobs     |  (Linking, Debug Symbols, Module Wrapping)
+-------------------+
          |
          v
+-------------------+
|   Executable      |  (Final Binary)
+-------------------+

Comprendre les modes de compilation Swift

Lors de la construction d’applications Swift—que ce soit pour une petite fonctionnalité ou pour optimiser une application en production—le comportement du compilateur varie considérablement en fonction du mode de compilation choisi. Ces modes permettent de trouver un équilibre entre la vitesse de compilation, les performances à l’exécution et l’efficacité du développement incrémental. Maîtriser ces modes est essentiel pour optimiser le temps de compilation Swift et améliorer les performances des builds dans Xcode.

Il existe trois modes de compilation principaux : Primary-file, Batch et Whole Module Optimization (WMO). Examinons-les en détail :

1. Mode Primary-file

Comme son nom l’indique, ce mode compile chaque fichier individuellement. Cette approche est idéale pour le débogage, car elle prend en charge la compilation incrémentale, où seuls les fichiers modifiés sont recompilés, et permet une parallélisation du travail sur plusieurs cœurs de CPU. Cependant, ce mode n’est pas parfaitement optimisé en raison de l’analyse syntaxique redondante : chaque job analyse tous les fichiers du module, ce qui entraîne une surcharge quadratique. Par exemple, avec 100 fichiers et 100 jobs, vous vous retrouvez avec 10 000 analyses syntaxiques.

Sous-modes :

  • Single-file : Un job de compilation par fichier. Par exemple, 10 fichiers entraînent 10 jobs, soit 10 000 analyses syntaxiques.
  • Batch : Regroupe les fichiers en lots par cœur de CPU. Par exemple, 10 fichiers sur 2 CPU créeraient 2 lots de 5 fichiers chacun.
# Single-file mode (default for Debug)  
swiftc *.swift -Onone -o MyApp

2. Mode Batch

Il s’agit d’une version améliorée du mode Primary-file qui réduit la surcharge liée à l’analyse syntaxique redondante en regroupant les fichiers en lots. Par exemple, 100 fichiers sur 4 CPU seraient répartis en 4 lots de 25 fichiers chacun. Le mode Batch conserve les avantages de la compilation incrémentale tout en étant plus efficace pour les grands projets, où la surcharge d’analyse syntaxique du mode Single-file devient prohibitif. Vous pouvez activer le mode Batch en utilisant le flag -enable-batch-mode.

# Batch mode (groups files per CPU)  
swiftc *.swift -enable-batch-mode -Onone -o MyApp  

3. Whole Module Optimization (WMO)

Ce mode compile l’ensemble du module en un seul job, permettant des optimisations avancées telles que l’élimination de code mort et l’inlining de fonctions. Il évite complètement l’analyse syntaxique redondante—par exemple, 100 fichiers nécessiteraient 1 job et 100 analyses syntaxiques. Cependant, le WMO présente deux inconvénients majeurs : il ne prend pas en charge la compilation incrémentale (ce qui impose des reconstructions complètes) et offre un parallélisme limité lors des premières étapes, comme les phases SIL/AST, qui sont mono-thread.

# Compile with WMO and optimizations  
swiftc *.swift -wmo -O -o MyApp  

4. Configuration dans Xcode

4.1. Configuration Debug :

  • Mode : Primary-file (single-file ou batch).
  • Optimisation : -Onone (aucune optimisation).
  • Activer le mode Batch : Ajoutez -enable-batch-mode dans Build Settings > Other Swift Flags.

4.2. Configuration Release :

  • Mode : WMO.
  • Optimisation : -O (optimisations agressives).
  • Activer le WMO : Allez dans Build Settings > Swift Compiler – Code Generation > Whole Module Optimization et réglez sur Yes.

Comparaison des temps de build

J’ai réalisé des tests de performance de compilation sur trois configurations de build Swift/Xcode : Single-file mode, Batch mode et WMO + Optimizations. Mon projet comprend 166 fichiers, et j’utilise un processeur Apple M3 Max avec 14 cœurs de CPU et 36 Go de mémoire. Voici les résultats :

ModeParsing OverheadIncremental
builds
Optimizations Example Build Time
Single-file27,556 parses
Élevé (quadratique)
-Onone1.32s user
0.52s system
2% cpu
1:17.32 total
Batch (14 CPUs)2016 parses
Réduit
-Onone1.14s user
0.41s system
2% cpu
1:16.21 total
WMO166 parses
Minimal
-O1.11s user
0.39s system
1% cpu
1:22.72 total
Pourquoi le WMO est-il plus lent ? Les optimisations comme l’élimination de code mort (dead code elimination) et l’inlining de fonctions augmentent le temps de compilation, mais elles produisent des binaires plus performants à l’exécution. Ces optimisations nécessitent une analyse approfondie de l’ensemble du module, ce qui explique pourquoi le WMO est plus lent que les autres modes. En résumé, le WMO sacrifie la rapidité de compilation pour maximiser les performances du code final.

Benchmarking des modes de compilation Swift : Single-File, Batch et WMO avec xcodebuild

J’ai comparé les performances de compilation Swift à travers les trois stratégies mentionnées en utilisant la commande xcodebuild.
Voici un aperçu de la manière dont vous pouvez mesurer les temps de build pour chaque mode :

# Single-file mode  
time xcodebuild -project YourProject.xcodeproj -scheme "YourSchemeName" clean build SWIFT_COMPILATION_MODE=singlefile SWIFT_OPTIMIZATION_LEVEL=-Onone 

# Batch mode  
time xcodebuild -project YourProject.xcodeproj -scheme "YourSchemeName" clean build SWIFT_COMPILATION_MODE=wholemodule SWIFT_OPTIMIZATION_LEVEL=-Onone 

# WMO + Optimizations
time xcodebuild -project YourProject.xcodeproj -scheme "YourSchemeName" clean build SWIFT_COMPILATION_MODE=wholemodule SWIFT_OPTIMIZATION_LEVEL=-O

Remarque : Les résultats peuvent varier en fonction de la taille du projet, de la structure des dépendances et du niveau de parallélisme de votre matériel.

En utilisant ces commandes, vous pouvez adapter votre configuration de build en fonction de vos besoins spécifiques, que ce soit pour un développement rapide ou pour des performances d’exécution optimales.

Vous pouvez également créer une configuration Debug-WMO pour tester le WMO avec -Onone. Cela vous permettra d’observer comment ce mode désactive les builds incrémentaux tout en réduisant la surcharge liée à l’analyse syntaxique.

Comment Boostez la Vitesse de Compilation Xcode avec les Modules Explicitement Construits

L’optimisation des temps de compilation dans Xcode est un enjeu majeur pour les développeurs iOS et macOS. Une solution avancée pour accélérer ce processus est l’utilisation des modules explicitement construits (Explicitly Built Modules). Contrairement à la découverte implicite des modules, cette approche permet de pré compiler les dépendances et de les stocker sous forme de produits binaires réutilisables. Résultat : des builds plus rapides, un parallélisme accru et une gestion optimisée des ressources système. En adoptant cette stratégie, vous réduisez le temps d’analyse des dépendances et minimisez les blocages liés aux accès disque, améliorant ainsi considérablement l’efficacité de vos développements sous Xcode, mais attention ce mode n’est pas efficaces sur tous les projets

On Compare les approches Implicit Module Discovery vs. Explicit Module Compilation

Pour cette étude, nous avons testé IceCubesApp, une application open-source avec de nombreuses dépendances Swift et Clang. En raison de cette architecture modulaire, nous avons comparé les performances entre la découverte implicite des modules et la compilation explicite des modules afin d’évaluer l’impact sur le temps de build.

Mode de CompilationTemps Total (s)CPU Utilisation (%)Observations
Implicit Module Build33.80s16%Meilleur parallélisme, gestion automatique des dépendances.
Explicit Module Build50.51s12%Plus lent en raison du pré traitement des dépendances Clang.

Observations

Vu comment l’application est développé les résultats montrent que Explicit Module Build est moins efficace en raison de son grand nombre de dépendances Clang (*.pcm).
Dans ce cas la compilation implicite reste plus efficace en permettant une meilleure parallélisation des tâches Toutefois, pour un projet avec principalement des modules Swift, la compilation explicite pourrait offrir des gains sur les builds
incrémentaux. Malheureusement la conception des modules SwiftPM sur ce projet n’était profondément réfléchie

Optimiser les Temps de Compilation sur Xcode : Bonnes Pratiques et Recommandations

Suite aux tests réalisés sur IceCubesApp, nous avons confirmé que la gestion des modules impacte directement le temps de compilation.
Selon un document publié par Artem Chikin (Swift Explicitly-Built Modules) et en le couplant avec les résultats de ma phase de recherche, voici quelques recommandations que je peux vous donner pour optimiser vos builds Xcode et éviter les ralentissements inutiles.

1 – Utiliser des Modules Pré compilés pour Éviter la Recompilation Inutile

L’un des moyens les plus efficaces pour accélérer la compilation est de précompiler vos modules afin d’éviter leur reconstruction systématique. Pour cela, vous pouvez utiliser Swift Explicitly Built Modules, qui permettent de générer et stocker les modules sous forme de fichiers binaires réutilisables.

💡 Conseil : Vérifiez votre dossier DerivedData (~/Library/Developer/Xcode/DerivedData/) pour voir si des fichiers *.pcm (Clang modules) sont générés en trop grand nombre, ce qui peut ralentir la compilation.

Si vous trouvez un trop grand nombre de fichiers *.pcm dans DerivedData, cela signifie que Xcode génère et recompiles excessivement des modules Clang, ce qui peut ralentir vos builds. Voici ce qu’il faut faire dans ce cas :

# commencez par nettoyer DerivedData pour forcer une recompilation plus efficace
rm -rf ~/Library/Developer/Xcode/DerivedData/*

# Vérifier les Modules Clang Générés Automatiquement
find ~/Library/Developer/Xcode/DerivedData/ -name "*.pcm"

# Désactiver la Compilation Inutile des Modules Clang
OTHER_CFLAGS="-fno-implicit-modules -fno-implicit-module-maps"

Pourquoi?

  • -fno-implicit-modules: Empêche Clang de générer automatiquement des fichiers de module (*.pcm), l’obligeant ainsi à utiliser des modules précompilés à la place..
  • -fno-implicit-module-maps: Garantit que les maps de modules sont définies explicitement plutôt que déduites, améliorant ainsi la stabilité de la compilation.

2 – Exploiter swiftc -emit-module pour une Gestion Efficace des Builds

Plutôt que de laisser Xcode gérer automatiquement les dépendances, vous pouvez générer manuellement les modules Swift avant la compilation principale. Cela réduit le temps d’analyse et permet une meilleure réutilisation des modules.

Commande pour générer un module Swift manuellement :

swiftc -emit-module -module-name MyModule -o MyModule.swiftmodule MyModule.swift

Avantages :

  • Évite de recompiler les modules à chaque build.
  • Améliore la cohérence des dépendances sur plusieurs configurations.
  • Réduit le nombre d’erreurs de linking dues aux dépendances.

Conclusion

En appliquant ces stratégies, vous pourrez réduire le temps de compilation Xcode et éviter les ralentissements dus à la découverte et la recompilation des modules. Chaque projet étant unique, l’optimisation dépendra du nombre de dépendances Swift et Clang utilisées.

En maîtrisant ces modes, on comprend que créer plus de fichiers (class / struct / enum) n’est pas forcément un atout, et que des fichiers trop longs ne le sont pas non plus. Par exemple, en mode WMO, un fichier très long peut ralentir la compilation, car le compilateur doit traiter une grande quantité de code en une seule fois. En approfondissant ces modes et leurs paramètres, on réalise que la meilleure façon d’optimiser le temps de build commence par la manière dont nous structurons et rédigeons notre code, en adoptant la bonne approche et en définissant le mode de configuration adapté à chaque module en vue de son utilisation.

C’est tout pour aujourd’hui ! J’espère que vous avez apprécié cette analyse.

Références et lectures complémentaires

Pour les lecteurs souhaitant explorer en profondeur l’architecture du compilateur Swift et les optimisations de performance, voici quelques ressources clés :

  1. Performances du compilateur Swift :
    Performances du compilateur Swift, incluant des détails sur les modes de compilation, les diagnostics et les flags d’optimisation.
  2. Apple’s Swift.org :
    Une vue d’ensemble de la conception du compilateur Swift et de son intégration avec LLVM.
  3. WWDC 2020: Dive Into Swift Compiler Diagnostics:
    Une session expliquant comment le compilateur traite le code et génère des diagnostics.
  4. LLVM Project:
    Apprenez-en plus sur LLVM, l’infrastructure backend qui alimente la génération de code et les optimisations de Swift.

Ces ressources fournissent des connaissances fondamentales pour maîtriser le pipeline de compilation de Swift. Bonne lecture et bon codage ! 🛠️

Categorized in:

Swift,