Vous avez peut-être remarqué ce réglage "Optimization Level" dans les Build Settings de votre projet Xcode. -Onone, -O, -Osize... C'est quoi tout ça ? Et surtout, pourquoi votre app rame en Debug à un tel point que vous avez envie de jeter votre telephone contre le mur mais vole en Release tel un albatros alpha ? Décortiquons ensemble ces options qui font une vraie différence.


1. C'est quoi l'optimisation du compilateur ?

Le principe

Quand vous écrivez du code Swift, le compilateur (swiftc) le transforme en code machine exécutable (donc en binaire). Entre votre code source et le binaire final, plusieurs passes intermédiaires ont lieu (SIL, ARC, LLVM), et le compilateur peut faire différents compromis :

  • Guarder la structure du code : facile à débug, mais peu performant
  • Réorganiser et spécialiser : plus rapide à l'exécution, mais plus difficile à debug
  • Réduire la taille du code généré : binaire plus petit, parfois légèrement moins rapide

Ces choix sont contrôlés par le ✨ niveau d'optimisation ✨.

Pourquoi ça compte ?

La différence est loin d'être anecdotique :

AspectSans optimisationAvec optimisation
Vitesse d'exécutionBaseline2× à 10× plus rapide
Taille du binairePlus grandeJusqu'à ~30% plus petite
Temps de compilationRapidePlus lent
DebuggingExcellentLimité

Les étapes principales du compilateur Swift rapidement

  1. AST (Abstract Syntax Tree)
    Représentation proche de votre code Swift.

  2. SIL (Swift Intermediate Language)
    Langage intermédiaire spécifique à Swift, où ont lieu :

    • ARC optimizations
    • inlining Swift-aware
    • spécialisation des generics
    • élimination des abstractions Swift
  3. LLVM IR
    Le SIL est abaissé en LLVM IR, où s'appliquent :

    • loop unrolling
    • vectorisation SIMD
    • optimisations bas niveau CPU
    • register allocation
  4. Code machine
    Génération du binaire final.

Vous ne comprenez pas tout ? Moi aussi, mais on apprendra ça ensemble dans un autre article histoire de mieux comprendre ce qu'il se passe sous le capot. L'idée était de vous montrer qu'il y a plusieurs processus lancés lors de la compilation et que vos réglages d'optimisations ont un impact sur ces éléments, on va voir ça tout de suite.

2. Les différents niveaux d'optimisation

Swift propose quatre niveaux d'optimisation principaux.

-Onone (No optimization)

Sur Xcode vous voyez :

Optimization Level = None [-Onone]

Ce que ça fait :

  • Aucune optimisation
  • Le code généré correspond à ce que vous avez écrit
  • Toutes les variables sont préservées
  • Tous les appels de fonctions sont explicites

Quand l'utiliser :

  • Pour débuger

Avantages :

  • Compilation rapide
  • Debugging parfait (breakpoints, variables)

Inconvénients :

  • Performances médiocres
  • Binaire plus volumineux

-O (Optimize for Speed)

Sur Xcode vous voyez :

Optimization Level = Optimize for Speed [-O]

Ce que ça fait :

  • Optimisations agressives pour la vitesse
  • Inlining des fonctions (le code est copié au lieu d'être appelé)
  • Élimination du code mort
  • Déroulage des boucles (loop unrolling)
  • Propagation des constantes
  • Et bien d'autres transformations...

Quand l'utiliser :

  • Release : c'est le choix par défaut et recommandé
  • Quand la performance est prioritaire
  • Pour la production

Avantages :

  • Performances optimales
  • Le meilleur rapport qualité/vitesse

Inconvénients :

  • Compilation plus lente
  • Debugging très limité (variables "optimized out")
  • Stack traces parfois incomplètes

-Osize (Optimize for Size)

Sur Xcode vous voyez :

Optimization Level = Optimize for Size [-Osize]

Ce que ça fait :

  • Optimise pour réduire la taille du binaire
  • Moins d'inlining (évite la duplication de code)
  • Préfère les appels de fonction aux copies
  • Garde certaines optimisations de vitesse quand elles ne gonflent pas le code

Quand l'utiliser :

  • Apps avec contraintes de taille (App Clips limités à 15 MB)
  • Extensions (widgets, notifications) avec budgets serrés
  • Marchés émergents où la taille de téléchargement compte

Avantages :

  • Binaire significativement plus petit (10-30% selon les cas)
  • Performances correctes (pas aussi rapide que -O, mais pas catastrophique)

Inconvénients :

  • Légèrement plus lent que -O
  • Debugging limité comme -O

-Ounchecked (Unchecked optimization)

Sur Xcode vous voyez :

Optimization Level = Optimize for Speed, Unchecked [-Ounchecked]

L'option dangereuse

Ce que ça fait :

  • Toutes les optimisations de -O

Supprime les vérifications de sécurité à l'exécution :

  • Débordements arithmétiques (overflow)
  • Accès aux tableaux hors limites
  • Force-unwrap de nil
  • Préconditions et assertions

Quand l'utiliser :

  • Quasi jamais en pratique
  • Code ultra-critique en performance déjà prouvé correct
  • Si Tanos reviens, quoi que j'ai un doute ...

Avantages :

  • Performances maximales théoriques
  • Peut faire la différence dans du code numérique intensif

Inconvénients :

  • Comportement indéfini si une vérification aurait échoué
  • Crashs silencieux, corruption mémoire possible
  • Très difficile à débugger
  • Failles de sécurité potentielles

Le conseil : oubliez que ça existe. Les gains sont marginaux et les risques présents. Si vous avez besoin de ce niveau de performance, vous savez déjà ce que vous faites et c'est surement pas un wrapper ChatGPT. (Si vous l'utilisez pour de très bonne raison hésitez pas à m'envoyer un petit mail je suis super curieuse de ce cas d'usage)

3. Whole Module Optimization (WMO)

Au-delà du niveau d'optimisation, il y a un autre réglage crucial : Whole Module Optimization.

C'est quoi ?

Par défaut, Swift compile chaque fichier séparément. Avec WMO, le compilateur voit tout le module d'un coup et peut faire des optimisations impossibles autrement.

Sur Xcode vous voyez :

Compilation Mode = Whole Module

Ce que ça permet

Sans WMO (fichier par fichier) :

// FileA.swift func helperFunction() -> Int { return 42 } // FileB.swift func mainFunction() -> Int { return helperFunction() // Le compilateur ne sait pas ce que fait helperFunction // il doit faire un vrai appel de fonction }

Avec WMO :

// Le compilateur voit les deux fichiers ensemble // il peut inliner helperFunction directement func mainFunction() -> Int { return 42 // Plus d'appel de fonction ! }

Optimisations permises par WMO

  • Cross-file inlining : inliner des fonctions définies dans d'autres fichiers
  • Dead code elimination globale : supprimer le code jamais appelé dans tout le module
  • Dévirtualisation : transformer des appels dynamiques en appels statiques
  • Spécialisation des generics : générer des versions optimisées pour les types concrets

Impact sur les performances

WMO peut apporter 10-20% de gains supplémentaires, parfois plus. C'est particulièrement efficace pour :

  • Les protocoles avec des implémentations connues
  • Les generics utilisés avec des types concrets
  • Le code bien structuré en petites fonctions

Le compromis : temps de compilation

ModeTemps de compilation incrémentalTemps de compilation clean
Fichier par fichierRapide (recompile uniquement les fichiers modifiés)Normal
Whole ModuleLent (recompile tout le module)Similaire

En Debug, vous voulez des builds incrémentaux rapides utilisez fichier par fichier. En Release, vous voulez les meilleures performances utilisez Whole Module.

Configuration dans Xcode

Le réglage se trouve dans Build Settings :

Compilation Mode: - Incremental (fichier par fichier) - Whole Module

Ou via le flag :

SWIFT_COMPILATION_MODE = wholemodule

4. Configurer dans Xcode

Via l'interface

  1. Ouvrez votre projet dans Xcode
  2. Sélectionnez votre target
  3. Onglet Build Settings
  4. Cherchez "Swift Compiler - Code Generation"

Vous trouverez :

SettingCléOptions
Optimization LevelSWIFT_OPTIMIZATION_LEVEL-Onone, -O, -Osize, -Ounchecked
Compilation ModeSWIFT_COMPILATION_MODEincremental, wholemodule

Xcode Build Settings - Swift Compiler Code Generation
Xcode Build Settings - Swift Compiler Code Generation

Les options d'Optimization Level disponibles :

Options d'optimisation Xcode
Options d'optimisation Xcode

Configuration recommandée

Par défaut, Xcode configure ça correctement :

ConfigurationOptimization LevelCompilation Mode
Debug-OnoneIncremental
Release-OWhole Module

Voir la configuration effective

Pour vérifier ce qui est réellement appliqué :

  1. Build Settings > vue "Levels"
  2. Vous voyez d'où vient chaque valeur (défaut, projet, target, xcconfig)

5. Configurer dans les .xcconfig

Comme pour les autres flags, centraliser dans les xcconfig est une bonne pratique.

Exemple de configuration

Config/Debug.xcconfig :

// Debug : pas d'optimisation, compilation rapide SWIFT_OPTIMIZATION_LEVEL = -Onone SWIFT_COMPILATION_MODE = incremental // Assertions actives SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG

Config/Release.xcconfig :

// Release : optimisations maximales SWIFT_OPTIMIZATION_LEVEL = -O SWIFT_COMPILATION_MODE = wholemodule // Pas d'assertions en prod SWIFT_ACTIVE_COMPILATION_CONDITIONS =

Config/ReleaseSmall.xcconfig (si vous avez besoin d'une config pour la taille) :

// Release optimisée pour la taille (App Clips, extensions) SWIFT_OPTIMIZATION_LEVEL = -Osize SWIFT_COMPILATION_MODE = wholemodule

Pour les extensions avec contraintes de taille

Si vous avez un App Clip ou un widget avec un budget taille serré :

// WidgetExtension.xcconfig SWIFT_OPTIMIZATION_LEVEL = -Osize SWIFT_COMPILATION_MODE = wholemodule // Activer aussi le stripping STRIP_STYLE = all DEPLOYMENT_POSTPROCESSING = YES

6. Impact concret sur votre code

Ce qui change avec les optimisations

Prenons un exemple concret pour comprendre ce que fait le compilateur.

Code source :

struct Point { var x: Double var y: Double func distance(to other: Point) -> Double { let dx = other.x - x let dy = other.y - y return sqrt(dx * dx + dy * dy) } } func totalDistance(_ points: [Point]) -> Double { var total = 0.0 for i in 0..<(points.count - 1) { total += points[i].distance(to: points[i + 1]) } return total }

Avec -Onone :

  • Chaque appel à distance(to:) est un vrai appel de fonction
  • Les variables dx, dy, total existent en mémoire
  • Les accès au tableau passent par des vérifications de bornes
  • Vous pouvez mettre un breakpoint n'importe où

Avec -O + WMO :

  • distance(to:) est inliné dans la boucle
  • Les calculs intermédiaires restent dans les registres CPU
  • La boucle peut être vectorisée (SIMD)
  • Les vérifications de bornes peuvent être sorties de la boucle ou éliminées
  • Le code final ressemble plus à du C optimisé

Debugging en mode optimisé

Quand vous debuggez une build Release, vous verrez souvent :

(lldb) po total error: <EXPR>:3:1: error: use of unresolved identifier 'total'

Ou dans le debugger :

total = <optimized out>

C'est normal : la variable n'existe plus, le compilateur l'a optimisée. C'est pourquoi on debug en Debug, pas en Release.

Assertions et préconditions

Le comportement des assertions change selon la config :

Fonction-Onone-O-Ounchecked
assert()✅ Actif❌ Ignoré❌ Ignoré
assertionFailure()✅ Crash❌ Ignoré❌ Ignoré
precondition()✅ Actif✅ Actif❌ Ignoré
preconditionFailure()✅ Crash✅ Crash❌ Ignoré
fatalError()✅ Crash✅ Crash✅ Crash

Conséquence :

func processIndex(_ index: Int, in array: [Int]) -> Int { // En Release avec -O, ce check est toujours exécuté precondition(index >= 0 && index < array.count, "Index out of bounds") // Celui-ci est ignoré en Release assert(index >= 0, "Negative index") // Debug only return array[index] }

Utilisez precondition pour les checks qui doivent rester en prod, assert pour ceux utiles uniquement en dev.


7. Cas d'usage avancés

Optimiser une target spécifique différemment

Vous pouvez avoir une config différente par target. Par exemple, votre framework de calcul intensif en -O même en Debug :

  1. Sélectionnez la target du framework
  2. Build Settings > Optimization Level
  3. Changez la valeur pour Debug en -O

Ou via xcconfig dédié au framework.

Builds de profiling

Pour profiler avec Instruments, vous voulez :

  • Les optimisations activées (sinon vous profilez du code lent)
  • Les symboles de debug disponibles (pour voir les noms de fonctions)

Xcode a une configuration Profile pour ça, ou vous pouvez créer la vôtre :

// Config/Profile.xcconfig SWIFT_OPTIMIZATION_LEVEL = -O SWIFT_COMPILATION_MODE = wholemodule // Garder les symboles pour le profiling DEBUG_INFORMATION_FORMAT = dwarf-with-dsym GCC_GENERATE_DEBUGGING_SYMBOLS = YES

Analyser la taille du binaire

Pour comprendre ce qui prend de la place et si -Osize vaut le coup :

# Taille des sections du binaire size -m MyApp.app/MyApp # Symboles triés par taille nm -S --size-sort MyApp.app/MyApp | tail -50

Ou utilisez l'outil Bloaty pour une analyse détaillée :

brew install bloaty bloaty MyApp.app/MyApp -d compileunits

8. Bons réflexes et erreurs fréquentes

Activer les optimisations en Debug

// On evite - Debug avec -O Debug: SWIFT_OPTIMIZATION_LEVEL = -O

Symptômes :

  • Breakpoints qui ne s'arrêtent pas où vous voulez
  • Variables "optimized out"
  • Stack traces incompréhensibles
  • Temps de compilation Debug allongé

Gardez -Onone en Debug.

Désactiver les optimisations en Release

// On evite - Release sans optimisation Release: SWIFT_OPTIMIZATION_LEVEL = -Onone

Votre app sera 2 à 10 fois plus lente que nécessaire. Les utilisateurs le sentiront.

Utiliser -Ounchecked "pour la perf"

Les gains sont marginaux et les risques présents, votre app peut crasher en silence en prod et c'est risquer des utilisateurs en moins sans en savoir la raison.

Oublier WMO en Release

// On evite - Release sans WMO Release: SWIFT_COMPILATION_MODE = incremental

Vous perdez 10-20% de performances gratuites.

Configuration type recommandée

ConfigOptimizationCompilation ModeUse case
Debug-OnoneIncrementalDev quotidien
Release-OWhole ModuleProduction
Profile-OWhole ModuleProfiling Instruments
ReleaseSmall-OsizeWhole ModuleApp Clips, extensions

Résumé visuel

OPTIMISATION ────────────────────────────────────────► -Onone -Osize -O -Ounchecked │ │ │ │ ▼ ▼ ▼ ▼ Aucune Taille Vitesse Vitesse max optimisation minimale maximale (dangereux) ✅ Debug ✅ App Clips ✅ Release ⛔ Éviter ✅ Debugging ✅ Extensions ✅ Production COMPILATION MODE ────────────────────────────────────────► Incremental Whole Module │ │ ▼ ▼ Fichier par fichier Module entier ✅ Debug (builds rapides) ✅ Release (meilleures optims)

Pour aller plus loin

Si vous voulez creuser le sujet :