-
Optimiser les performances du processeur avec Instruments
Découvrez comment optimiser votre app pour les puces Apple à l'aide de deux nouveaux outils d'assistance matérielle dans Instruments. Nous commencerons par expliquer comment profiler votre app, puis nous approfondirons en montrant chaque fonction appelée avec Processor Trace. Nous verrons également comment utiliser les modes Compteurs de processeur pour analyser votre code à la recherche de goulots d'étranglement du processeur.
Chapitres
- 0:00 - Introduction et ordre du jour
- 2:28 - État d’esprit de performance
- 8:50 - Profileurs
- 13:20 - Étendue
- 14:05 - Processor Trace
- 19:51 - Analyse des goulots d’étranglement
- 31:33 - Récapitulatif
- 32:13 - Étapes suivantes
Ressources
- Performance and metrics
- Analyzing CPU usage with the Processor Trace instrument
- Apple Silicon CPU Optimization Guide Version 4
- Tuning your code’s performance for Apple silicon
Vidéos connexes
WWDC25
- Améliorer l’utilisation de la mémoire et les performances avec Swift
- Optimiser les performances de SwiftUI avec Instruments
- Profilez et optimisez la consommation d’énergie de votre app
WWDC24
WWDC23
WWDC22
-
Rechercher dans cette vidéo…
-
-
6:37 - Binary search in Collection
public func binarySearch<E, C>( needle: E, haystack: C ) -> C.Index where E: Comparable, C: Collection<E> { var start = haystack.startIndex var length = haystack.count while length > 0 { let half = length / 2 let middle = haystack.index(start, offsetBy: half) let middleValue = haystack[middle] if needle < middleValue { length = half } else if needle == middleValue { return middle } else { start = haystack.index(after: middle) length -= half + 1 } } return start } -
7:49 - Throughput benchmark
import Testing import OSLog let signposter = OSSignposter( subsystem: "com.example.apple-samplecode.MyBinarySearch", category: .pointsOfInterest ) func search( name: StaticString, duration: Duration, _ search: () -> Void ) { var now = ContinuousClock.now var outerIterations = 0 let interval = signposter.beginInterval(name) let start = ContinuousClock.now repeat { search() outerIterations += 1 now = .now } while (start.duration(to: now) < duration) let elapsed = start.duration(to: now) let seconds = Double(elapsed.components.seconds) + Double(elapsed.components.attoseconds) / 1e18 let throughput = Double(outerIterations) / seconds signposter.endInterval(name, interval, "\(throughput) ops/s") print("\(name): \(throughput) ops/s") } let arraySize = 8 << 20 let arrayCount = arraySize / MemoryLayout<Int>.size let searchCount = 10_000 struct MyBinarySearchTests { let sortedArray: [Int] let randomElements: [Int] init() { let sortedArray: [Int] = (0..<arrayCount).map { _ in .random(in: 0..<arrayCount) }.sorted() self.randomElements = (0..<searchCount).map { _ in sortedArray.randomElement()! } self.sortedArray = sortedArray } @Test func searchCollection() throws { search(name: "Collection", duration: .seconds(1)) { for element in randomElements { _ = binarySearch(needle: element, haystack: sortedArray) } } } } -
13:46 - Binary search in Span
public func binarySearch<E: Comparable>( needle: E, haystack: Span<E> ) -> Span<E>.Index { var start = haystack.indices.startIndex var length = haystack.count while length > 0 { let half = length / 2 let middle = haystack.indices.index(start, offsetBy: half) let middleValue = haystack[middle] if needle < middleValue { length = half } else if needle == middleValue { return middle } else { start = haystack.indices.index(after: middle) length -= half + 1 } } return start } -
15:09 - Throughput benchmark for binary search in Span
extension MyBinarySearchTests { @Test func searchSpan() throws { let span = sortedArray.span search(name: "Span", duration: .seconds(1)) { for element in randomElements { _ = binarySearch(needle: element, haystack: span) } } } @Test func searchSpanForProcessorTrace() throws { let span = sortedArray.span signposter.withIntervalSignpost("Span") { for element in randomElements[0..<10] { _ = binarySearch(needle: element, haystack: span) } } } } -
19:17 - Binary search in Span
public func binarySearchInt( needle: Int, haystack: Span<Int> ) -> Span<Int>.Index { var start = haystack.indices.startIndex var length = haystack.count while length > 0 { let half = length / 2 let middle = haystack.indices.index(start, offsetBy: half) let middleValue = haystack[middle] if needle < middleValue { length = half } else if needle == middleValue { return middle } else { start = haystack.indices.index(after: middle) length -= half + 1 } } return start } -
23:04 - Throughput benchmark for binary search in Span
extension MyBinarySearchTests { @Test func searchSpanInt() throws { let span = sortedArray.span search(name: "Span<Int>", duration: .seconds(1)) { for element in randomElements { _ = binarySearchInt(needle: element, haystack: span) } } } } -
26:34 - Branchless binary search
public func binarySearchBranchless( needle: Int, haystack: Span<Int> ) -> Span<Int>.Index { var start = haystack.indices.startIndex var length = haystack.count while length > 0 { let remainder = length % 2 length /= 2 let middle = start &+ length let middleValue = haystack[middle] if needle > middleValue { start = middle &+ remainder } } return start } -
27:20 - Throughput benchmark for branchless binary search
extension MyBinarySearchTests { @Test func searchBranchless() throws { let span = sortedArray.span search(name: "Branchless", duration: .seconds(1)) { for element in randomElements { _ = binarySearchBranchless(needle: element, haystack: span) } } } } -
29:27 - Eytzinger binary search
public func binarySearchEytzinger( needle: Int, haystack: Span<Int> ) -> Span<Int>.Index { var start = haystack.indices.startIndex.advanced(by: 1) let length = haystack.count while start < length { let value = haystack[start] start *= 2 if value < needle { start += 1 } } return start >> ((~start).trailingZeroBitCount + 1) } -
30:34 - Throughput benchmark for Eytzinger binary search
struct MyBinarySearchEytzingerTests { let eytzingerArray: [Int] let randomElements: [Int] static func reorderEytzinger(_ input: [Int], array: inout [Int], sourceIndex: Int, resultIndex: Int) -> Int { var sourceIndex = sourceIndex if resultIndex < array.count { sourceIndex = reorderEytzinger(input, array: &array, sourceIndex: sourceIndex, resultIndex: 2 * resultIndex) array[resultIndex] = input[sourceIndex] sourceIndex = reorderEytzinger(input, array: &array, sourceIndex: sourceIndex + 1, resultIndex: 2 * resultIndex + 1) } return sourceIndex } init() { let sortedArray: [Int] = (0..<arrayCount).map { _ in .random(in: 0..<arrayCount) }.sorted() var eytzingerArray: [Int] = Array(repeating: 0, count: arrayCount + 1) _ = Self.reorderEytzinger(sortedArray, array: &eytzingerArray, sourceIndex: 0, resultIndex: 1) self.randomElements = (0..<searchCount).map { _ in sortedArray.randomElement()! } self.eytzingerArray = eytzingerArray } @Test func searchEytzinger() throws { let span = eytzingerArray.span search(name: "Eytzinger", duration: .seconds(1)) { for element in randomElements { _ = binarySearchEytzinger(needle: element, haystack: span) } } } }
-
-
- 0:00 - Introduction et ordre du jour
L'optimisation du code pour les CPU à puce Apple est complexe en raison des couches d'abstraction entre le code source Swift et les instructions-machine, ainsi que des modes d'exécution désordonnés des instructions et de l'utilisation des caches mémoires par les CPU. Instruments aide les développeurs à naviguer dans cette complexité en facilitant l'analyse des performances et le profilage du système pour identifier une utilisation excessive du CPU. Utilisez les instruments Processor Trace et CPU Counters pour enregistrer les instructions, mesurer les coûts et analyser les goulots d'étranglement, ce qui conduit à un code plus efficace et de meilleures performances applicatives.
- 2:28 - État d’esprit de performance
Lors de l'analyse de problèmes de performance, il est essentiel de garder l'esprit ouvert et de recueillir des données pour valider les hypothèses. Les ralentissements peuvent venir de plusieurs facteurs, comme des threads bloqués sur des ressources, une mauvaise utilisation des API ou des algorithmes inefficaces. Des outils comme la jauge de CPU intégrée de Xcode ainsi que les instruments System Trace et Hangs dans Instruments sont précieux pour identifier les habitudes d'utilisation CPU, les blocages et les problèmes de réactivité de l'interface. Avant de plonger dans les micro-optimisations, qui compliquent souvent la maintenance du code, il vaut mieux explorer d'autres approches. Ces alternatives incluent : éviter les calculs inutiles, différer les traitements via la concurrence, précalculer certaines valeurs, ou mettre en cache les états issus d'opérations coûteuses. Si ces stratégies sont épuisées, il devient alors nécessaire d'optimiser le code lié au CPU. Concentrez-vous sur le code ayant un fort impact sur l'expérience utilisateur, notamment le chemin critique des interactions. Il est recommandé d'optimiser de manière incrémentale, en mesurant les progrès via des tests automatisés et des métriques dans Xcode et Instruments.
- 8:50 - Profileurs
Pour analyser les performances CPU de l'exemple de recherche binaire présenté, deux profileurs sont proposés dans Instruments : Time Profiler et CPU Profiler. Time Profiler échantillonne périodiquement l'activité CPU, mais peut subir des effets d'aliasing, déformant la représentation de l'activité réelle. CPU Profiler, quant à lui, échantillonne les CPU de façon indépendante en fonction de leur fréquence d'horloge, une méthode plus précise pour l'optimisation CPU. Dans cette analyse, CPU Profiler est lancé depuis le navigateur de test de Xcode, avec un enregistrement en mode différé dans Instruments pour limiter la surcharge. Les zones d'Instruments sont introduites : la vue chronologique, les pistes et les voies, et la vue détaillée qui présente les résultats du profilage. En examinant les pistes Points of Interest et Process, on repère la zone où les recherches binaires sont effectuées dans l'exemple d'application. L'arborescence des appels indique que les fonctions liées au protocole Collection consomment beaucoup de temps CPU. Une optimisation consiste à utiliser un type de conteneur plus efficace, tel que Span, pour éviter la surcharge des Array (sémantique copie sur écriture) et génériques.
- 13:20 - Étendue
Span, introduit dans Swift 6.2, est une structure de données efficiente représentant une plage de mémoire contiguë. Utiliser Span comme type d'entrée/sortie pour la recherche binaire permet une amélioration de performance de 400 % sans changer l'algorithme. Ensuite, pour optimiser davantage les performances, l'instrument Processor Trace est utilisé afin d'analyser la surcharge de vérification des limites.
- 14:05 - Processor Trace
Instruments 16.3 a introduit un nouvel instrument important appelé Processor Trace. Il permet de tracer toutes les instructions exécutées par le processus de votre app dans l'espace utilisateur sur Mac et iPad Pro avec puces M4 (et versions ultérieures), ou iPhone avec puces A18 (et versions ultérieures). Processor Trace nécessite l'activation de certains réglages spécifiques sur l'appareil. Il est particulièrement efficace pour des sessions de traçage courtes, car il peut générer une quantité importante de données. En enregistrant chaque décision de branchement, chaque nombre de cycles et l'heure actuelle, Instruments reconstruit le chemin d'exécution exact de l'app. Les données sont affichées sous forme de graphique de flamme, montrant la consommation de chaque appel de fonction. Contrairement aux graphiques de flamme traditionnels qui utilisent l'échantillonnage, le graphique de flamme de Processor Trace fournit une représentation exacte de la manière dont le CPU a exécuté le code. Cela vous permet d'identifier les goulots d'étranglement de performance avec une précision sans précédent. L'analyse des données de trace montre que la surcharge des métadonnées de protocole et l'incapacité à effectuer des comparaisons numériques provoquent d'importants ralentissements dans une fonction de recherche binaire spécifique. Pour remédier à cela, la fonction est spécialisée manuellement pour le type Int, ce qui améliore la performance d'environ 170 %. Cependant, une optimisation supplémentaire est nécessaire, car l'implémentation de la recherche binaire de l'app continue de ralentir l'ensemble.
- 19:51 - Analyse des goulots d’étranglement
Les CPU à puce Apple exécutent les instructions en deux phases : Instruction Delivery (livraison des instructions) et Instruction Processing (traitement des instructions), qui sont canalisés pour permettre le parallélisme au niveau de l'instruction. Cela permet le traitement simultané de plusieurs opérations, maximisant l'efficacité. Cependant, des goulots d'étranglement peuvent survenir dans le pipeline, ce qui bloque certaines opérations et réduit le parallélisme. L'instrument CPU Counters aide à identifier ces goulots en comptant les événements dans chaque unité CPU. Il utilise des modes prédéfinis pour mesurer les performances du CPU et répartir les tâches par grandes catégories. En analysant les données échantillonnées, on peut repérer les instructions problématiques, comme les erreurs de prédiction de branchement, qui entraînent des cycles gaspillés et une dégradation des performances. Les CPU exécutent les instructions hors ordre en s'appuyant sur des prédicteurs de branchement pour améliorer les performances. Cependant, des branches aléatoires peuvent induire ces prédicteurs en erreur. Pour y remédier, le code est réécrit afin d'éviter les branches difficiles à prédire. Le résultat est une recherche binaire sans branche, environ deux fois plus rapide. L'optimisation de l'app se concentre ensuite sur les accès mémoire, car les CPU utilisent une hiérarchie de caches pour accélérer l'accès aux données. Le schéma d'accès de l'algorithme de recherche binaire était sous-optimal pour cette hiérarchie, entraînant de nombreuses erreurs de cache. En réorganisant les éléments du tableau selon une disposition dite Eytzinger, la localité de cache est améliorée. Cela permet de rendre la recherche binaire encore 200 % plus rapide. Malgré ces optimisations significatives, le code reste techniquement limité par le traitement des instructions. Cependant, la fonction de recherche a été accélérée d'environ 2 500 % grâce au profilage et à des micro-optimisations.
- 31:33 - Récapitulatif
En mesurant d'abord les surcharges logicielles, puis en ciblant les goulots d'étranglement du CPU, la performance de l'app a été améliorée grâce à une approche attentive à l'architecture matérielle.
- 32:13 - Étapes suivantes
Pour optimiser vos apps, utilisez Instruments pour collecter des données, effectuer des tests de performance, et analyser les résultats. Vous pouvez également regarder les sessions sur les performances de Swift, et lire le guide « Apple Silicon CPU Optimization » dans la documentation développeur. Posez vos questions ou envoyez vos retours sur les Forums des développeurs.