-
Améliorer l’utilisation de la mémoire et les performances avec Swift
Découvrez comment améliorer les performances et la gestion de la mémoire de votre code Swift. Nous explorerons des moyens d'affiner votre code - de l'apport de modifications algorithmiques de haut niveau à l'adoption des nouveaux types InlineArray et Span pour un contrôle plus fin de la mémoire et des allocations.
Chapitres
- 0:00 - Introduction et ordre du jour
- 1:19 - Format QOI et app d’analyse
- 2:25 - Algorithmes
- 8:17 - Allocations
- 16:30 - Exclusivité
- 19:12 - Pile versus tas
- 21:08 - Comptage de références
- 29:52 - Bibliothèque d’analyse binaire Swift
- 31:03 - Étapes suivantes
Ressources
Vidéos connexes
WWDC25
- Nouveautés dans Swift
- Optimiser les performances du processeur avec Instruments
- Profilez et optimisez la consommation d’énergie de votre app
WWDC24
-
Rechercher dans cette vidéo…
-
-
7:01 - Corrected Data.readByte() method
import Foundation extension Data { /// Consume a single byte from the start of this data. mutating func readByte() -> UInt8? { guard !isEmpty else { return nil } return self.popFirst() } } -
9:56 - RGBAPixel.data(channels:) method
extension RGBAPixel { /// Returns the RGB or RGBA values for this pixel, as specified /// by the given channels information. func data(channels: QOI.Channels) -> some Collection<UInt8> { switch channels { case .rgb: [r, g, b] case .rgba: [r, g, b, a] } } } -
10:21 - Original QOIParser.parseQOI(from:) method
extension QOIParser { /// Parses an image from the given QOI data. func parseQOI(from input: inout Data) -> QOI? { guard let header = QOI.Header(parsing: &input) else { return nil } let pixels = readEncodedPixels(from: &input) .flatMap { decodePixels(from: $0) } .prefix(header.pixelCount) .flatMap { $0.data(channels: header.channels) } return QOI(header: header, data: Data(pixels)) } } -
12:53 - Revised QOIParser.parseQOI(from:) method
extension QOIParser { /// Parses an image from the given QOI data. func parseQOI(from input: inout Data) -> QOI? { guard let header = QOI.Header(parsing: &input) else { return nil } let totalBytes = header.pixelCount * Int(header.channels.rawValue) var pixelData = Data(repeating: 0, count: totalBytes) var offset = 0 while offset < totalBytes { guard let nextPixel = parsePixel(from: &input) else { break } switch nextPixel { case .run(let count): for _ in 0..<count { state.previousPixel .write(to: &pixelData, at: &offset, channels: header.channels) } default: decodeSinglePixel(from: nextPixel) .write(to: &pixelData, at: &offset, channels: header.channels) } } return QOI(header: header, data: pixelData) } } -
15:07 - Array behavior
var array = [1, 2, 3] array.append(4) array.removeFirst() // array == [2, 3, 4] var copy = array copy[0] = 10 // copy happens on mutation // array == [2, 3, 4] // copy == [10, 3, 4] -
19:47 - InlineArray behavior (part 1)
var array: InlineArray<3, Int> = [1, 2, 3] array[0] = 4 // array == [4, 2, 3] // Can't append or remove elements array.append(4) // error: Value of type 'InlineArray<3, Int>' has no member 'append' // Can only assign to a same-sized inline array let bigger: InlineArray<6, Int> = array // error: Cannot assign value of type 'InlineArray<3, Int>' to type 'InlineArray<6, Int>' -
20:23 - InlineArray behavior (part 2)
var array: InlineArray<3, Int> = [1, 2, 3] array[0] = 4 // array == [4, 2, 3] var copy = array // copy happens on assignment for i in copy.indices { copy[i] += 10 } // array == [4, 2, 3] // copy == [14, 12, 13] -
23:13 - processUsingBuffer() function
// Safe usage of a buffer pointer func processUsingBuffer(_ array: [Int]) -> Int { array.withUnsafeBufferPointer { buffer in var result = 0 for i in 0..<buffer.count { result += calculate(using: buffer, at: i) } return result } } -
23:34 - Dangerous getPointerToBytes() function
// Dangerous - DO NOT USE! func getPointerToBytes() -> UnsafePointer<UInt8> { let array: [UInt8] = Array(repeating: 0, count: 128) // DANGER: The next line escapes a pointer let pointer = array.withUnsafeBufferPointer { $0.baseAddress! } // DANGER: The next line returns the escaped pointer return pointer } -
24:46 - processUsingSpan() function
// Safe usage of a span @available(macOS 16.0, *) func processUsingSpan(_ array: [Int]) -> Int { let intSpan = array.span var result = 0 for i in 0..<intSpan.count { result += calculate(using: intSpan, at: i) } return result } -
25:07 - getHiddenSpanOfBytes() function (attempt 1)
@available(macOS 16.0, *) func getHiddenSpanOfBytes() -> Span<UInt8> { } // error: Cannot infer lifetime dependence... -
25:28 - getHiddenSpanOfBytes() function (attempt 2)
@available(macOS 16.0, *) func getHiddenSpanOfBytes() -> () -> Int { let array: [UInt8] = Array(repeating: 0, count: 128) let span = array.span return { span.count } } -
26:27 - RawSpan.readByte() method
@available(macOS 16.0, *) extension RawSpan { mutating func readByte() -> UInt8? { guard !isEmpty else { return nil } let value = unsafeLoadUnaligned(as: UInt8.self) self = self._extracting(droppingFirst: 1) return value } } -
28:02 - Final QOIParser.parseQOI(from:) method
/// Parses an image from the given QOI data. mutating func parseQOI(from input: inout RawSpan) -> QOI? { guard let header = QOI.Header(parsing: &input) else { return nil } let totalBytes = header.pixelCount * Int(header.channels.rawValue) let pixelData = Data(rawCapacity: totalBytes) { outputSpan in while outputSpan.count < totalBytes { guard let nextPixel = parsePixel(from: &input) else { break } switch nextPixel { case .run(let count): for _ in 0..<count { previousPixel .write(to: &outputSpan, channels: header.channels) } default: decodeSinglePixel(from: nextPixel) .write(to: &outputSpan, channels: header.channels) } } } return QOI(header: header, data: pixelData) } -
28:31 - RGBAPixel.write(to:channels:) method
@available(macOS 16.0, *) extension RGBAPixel { /// Writes this pixel's RGB or RGBA data into the given output span. @lifetime(&output) func write(to output: inout OutputRawSpan, channels: QOI.Channels) { output.append(r) output.append(g) output.append(b) if channels == .rgba { output.append(a) } } }
-
-
- 0:00 - Introduction et ordre du jour
Découvrez comment optimiser les performances des apps et bibliothèques Swift à l’aide de Swift 6.2. Les nouveaux types InlineArray et Span réduisent les allocations, les vérifications d’exclusivité et le comptage de références. Une nouvelle bibliothèque Swift open source, Binary Parsing, est introduite pour une analyse binaire rapide et sécurisée.
- 1:19 - Format QOI et app d’analyse
L’app de cette session WWDC25 charge les images au format QOI, un format simple et sans perte, avec une spécification d’une seule page. L’analyseur d’images de l’app prend en charge diverses méthodes d’encodage des pixels. Ensuite, l’app charge instantanément un petit fichier d’icône, mais il faut quelques secondes pour charger une photo plus grande d’un oiseau.
- 2:25 - Algorithmes
Lorsque les apps fonctionnent avec des données réelles, une utilisation incorrecte des algorithmes ou des structures de données peut nuire aux performances. Pour identifier ces problèmes et y remédier, vous pouvez utiliser Instruments, qui fournit des modèles d’instruments destinés à l’analyse des allocations et des versions, et qui permet d’identifier le code inefficace à l’aide de profileurs. L’instrument Time Profiler est particulièrement utile pour remédier aux problèmes de performances. En analysant les appels capturés et les traces de pile, vous pouvez identifier les zones où les apps passent le plus de temps. Dans l’exemple, un temps considérable a été consacré à platform_memmove, un appel système pour la copie de données. À l’aide d’Instruments, cet exemple analyse une méthode personnalisée nommée readByte. Cette méthode a été ajoutée à une extension du type Data, ce qui entraînait une copie excessive des données binaires. Dans l’exemple, la méthode est remplacée par la méthode popFirst(), plus efficace, qui réduit les données à partir du début d’une séquence sans les copier. Cette modification a résolu le problème de performances dans la méthode readByte. Après avoir effectué la modification, le profil a été exécuté à nouveau et la barre platform_memmove, qui occupait une place conséquente, a disparu du flamegraph. Les tests comparatifs ont révélé une accélération considérable, et la relation entre la taille de l’image et le temps d’analyse est passée de quadratique à linéaire, ce qui indique un algorithme plus efficace.
- 8:17 - Allocations
L’app est à nouveau profilée et il s’avère que l’analyseur d’images entraîne des allocations et des désallocations de mémoire excessives, en particulier au niveau des tableaux. Le nombre élevé d’allocations, près d’un million pour l’analyse d’une seule image, indique un problème critique. La plupart de ces allocations sont temporaires et de courte durée, ce qui suggère qu’elles peuvent être optimisées. Pour identifier la source de ces allocations inutiles, l’exemple utilise l’instrument Allocations dans Instruments. L’analyse révèle qu’une méthode appelée RGBAPixel.data(channels:) est en grande partie à l’origine du problème. Cette méthode crée un tableau chaque fois qu’elle est appelée, ce qui entraîne un nombre important d’allocations. La structure du code, qui implique une chaîne complexe de méthodes flatMap et prefix, contribue au problème. Chaque étape de cette chaîne entraîne de nouvelles allocations, car les tableaux sont créés, aplatis et copiés à plusieurs reprises. Bien que cette approche soit concise, elle n’est pas efficace en termes de mémoire. Pour résoudre ce problème, l’exemple réécrit la fonction d’analyse. Au lieu de s’appuyer sur des allocations intermédiaires, il calcule à l’avance la taille totale des données résultantes et alloue un seul tampon. Cette approche élimine la nécessité d’allocations répétées pendant le processus de décodage.
- 16:30 - Exclusivité
Les performances de l’app se sont tellement améliorées que les outils de profilage ont eu besoin de plus de données. Après avoir répété le code d’analyse 50 fois, les résultats ont affiché les symboles swift_beginAccess et swift_endAccess, qui indiquent des tests d’exclusivité. Ces tests d’exclusivité ont été provoqués par des propriétés de la classe State imbriquées dans la structure QOIParser, que l’exemple déplace ensuite directement dans le type d’analyseur parent afin d’éliminer les vérifications d’exclusivité. Après quelques ajustements du compilateur, la vérification de l’exclusivité a été entièrement supprimée, comme l’a confirmé un nouveau profil.
- 19:12 - Pile versus tas
Cet exemple remplace l’utilisation d’Array dans l’app par InlineArray, une collection de taille fixe stockée et intégrée, qui optimise l’utilisation de la mémoire en éliminant le comptage de références et les vérifications d’exclusivité. Cette méthode est idéale pour le cache de pixels, un tableau de 64 éléments dont la taille ne change jamais et qui est modifié sur place, car cela améliore les performances sans nécessiter de copie ou de partage de références.
- 21:08 - Comptage de références
Dans l’exemple d’optimisation finale de l’app, les nouveaux types Span sont utilisés pour optimiser les performances et renforcer la sécurité de la mémoire. Dans Instruments, le flamegraph est utilisé à partir de l’analyse Time Profiler. Les données profilées se concentrent sur le QOIParser et permettent d’identifier qu’un temps significatif est consacré aux opérations de comptage de références, en particulier avec le type Data, en raison de sa sémantique de copie à l’écriture. Span et ses types associés constituent une nouvelle façon de travailler avec la mémoire contiguë dans une collection. Ils utilisent la fonctionnalité non échappable (~Escapable) de Swift, qui lie leur durée de vie à la collection, garantissant ainsi la sécurité de la mémoire et éliminant la nécessité d’une gestion manuelle de la mémoire. Cela permet un accès efficace à la mémoire sans les risques associés aux pointeurs non sécurisés. L’exemple montre comment utiliser les types Span pour réécrire des méthodes existantes, afin de les rendre plus simples, plus sécurisées et plus performantes. Dans les méthodes d’analyse d’images, Data est remplacé par RawSpan, ce qui réduit considérablement la surcharge liée au comptage de références. De plus, OutputSpan est adopté dans le processus d’analyse pour plus d’efficacité, ce qui rend l’opération d’analyse six fois plus rapide qu’auparavant sans avoir recours à des pointeurs non sécurisés.
- 29:52 - Bibliothèque d’analyse binaire Swift
Swift Binary Parsing vous permet de créer des analyseurs syntaxiques sûrs et efficaces pour les formats binaires. Il offre des outils pour gérer divers aspects liés à la sécurité, notamment la prévention des dépassements d’entier, la spécification du signe, de la largeur de bit et de l’ordre des octets, ainsi que la validation des types personnalisés. La bibliothèque est déjà utilisée chez Apple et est accessible publiquement. Vous pouvez l’essayer et y contribuer via les forums Swift et GitHub.
- 31:03 - Étapes suivantes
Les principaux points à retenir sont les suivants : Utilisation de Xcode et Instruments pour le profilage des apps. Analyse des performances des algorithmes afin d’identifier les goulots d’étranglement. Exploration de solutions aux problèmes susmentionnés avec les nouveaux types InlineArray et Span introduits dans Swift 6.2.