-
Mejora el uso y el rendimiento de la memoria con Swift
Descubre formas de mejorar el rendimiento y la gestión de la memoria de tu código Swift. Exploraremos formas de perfeccionar tu código, desde realizar cambios algorítmicos de alto nivel hasta adoptar InlineArray y Span, para un control preciso de la memoria y las asignaciones.
Capítulos
- 0:00 - Introducción y agenda
- 1:19 - App de formato y parser QOI
- 2:25 - Algoritmos
- 8:17 - Asignaciones
- 16:30 - Exclusividad
- 19:12 - Pila contra montón
- 21:08 - Recuento de referencias
- 29:52 - Biblioteca de análisis binario Swift
- 31:03 - Próximos pasos
Recursos
Videos relacionados
WWDC25
- Crear perfiles y optimizar el uso de energía en tu app
- Novedades de Swift
- Optimiza el rendimiento del CPU con instrumentos
WWDC24
-
Buscar este video…
-
-
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 - Introducción y agenda
Obtenga información sobre cómo optimizar el rendimiento de las apps y bibliotecas de código Swift utilizando Swift 6.2. Los nuevos tipos 'InlineArray' y 'Span' reducen las asignaciones, las comprobaciones de exclusividad y el recuento de referencias. Se presenta una nueva biblioteca Swift de código abierto, Binary Parsing, para un análisis binario rápido y seguro.
- 1:19 - App de formato y parser QOI
La app de esta sesión de la WWDC25 carga imágenes en formato QOI, un formato simple y sin pérdidas con una especificación de una sola página. El analizador de imágenes de la app gestiona varios métodos de codificación de píxeles. Luego, la app carga instantáneamente un pequeño archivo de ícono, pero tarda unos segundos en cargar una foto más grande de un pájaro.
- 2:25 - Algoritmos
Cuando las apps trabajan con datos del mundo real, a menudo pueden surgir problemas de rendimiento debido al uso incorrecto de algoritmos o estructuras de datos. Para identificar y abordar estos problemas, puede utilizar Instrumentos, que tiene plantillas de instrumentos para analizar asignaciones y liberaciones e identificar código ineficiente con perfiladores. La herramienta Time Profiler es especialmente útil para problemas de rendimiento. Al analizar las llamadas capturadas y los seguimientos de pila, puede identificar las áreas donde las apps pasan la mayor parte del tiempo. En el ejemplo, se gastó una cantidad significativa de tiempo en una llamada del sistema para copiar datos, 'platform_memmove'. Mediante el uso de instrumentos, este ejemplo analiza un método personalizado llamado "readByte". Este método se agregó a una extensión del tipo 'Datos', lo que provocó una copia excesiva de datos binarios. El ejemplo reemplaza el método con el método 'popFirst()' más eficiente, que reduce los datos del frente de una secuencia sin copiar. Este cambio resolvió el problema de rendimiento en el método 'readByte'. Después de realizar el cambio, el ejemplo ejecutó el perfil nuevamente y la barra significativa 'platform_memmove' desapareció del gráfico de llama. La evaluación comparativa mostró una aceleración sustancial y la relación entre el tamaño de la imagen y el tiempo de análisis cambió de cuadrática a lineal, lo que indica un algoritmo más eficiente.
- 8:17 - Asignaciones
Se vuelve a perfilar la app para descubrir que el analizador de imágenes genera asignaciones y desasignaciones de memoria excesivas, en particular cuando involucran matrices. La gran cantidad de asignaciones, casi un millón para analizar una sola imagen, indica un problema crítico. La mayoría de estas asignaciones son transitorias y de corta duración, lo que sugiere que pueden optimizarse. Para identificar la fuente de estas asignaciones innecesarias, el ejemplo utiliza el instrumento Asignaciones en Instrumentos. El análisis revela que un método llamado 'RGBAPixel.data(channels:)' es el principal culpable. Este método crea una matriz cada vez que se llama, lo que genera una cantidad sustancial de asignaciones. La estructura del código, que implica una cadena compleja de métodos 'flatMap' y 'prefix', contribuye al problema. Cada paso de esta cadena genera nuevas asignaciones a medida que las matrices se crean, se aplanan y se copian repetidamente. Si bien este enfoque es conciso, no es eficiente en el uso de la memoria. Para solucionar este problema, el ejemplo reescribe la función de análisis. En lugar de depender de asignaciones intermedias, calcula de antemano el tamaño total de los datos del resultado y asigna un único búfer. Este enfoque elimina la necesidad de asignaciones repetidas durante el proceso de decodificación.
- 16:30 - Exclusividad
El rendimiento de la app mejoró tanto que los instrumentos de creación de perfiles necesitaban más datos. Después de repetir el código de análisis 50 veces, los resultados mostraron los símbolos 'swift_beginAccess' y 'swift_endAccess' que indican pruebas de exclusividad. Estas pruebas de exclusividad fueron causadas por propiedades en la clase 'State' anidadas dentro de la estructura 'QOIParser', que el ejemplo luego mueve directamente al tipo de analizador padre para eliminar las verificaciones de exclusividad. Después de algunos ajustes del compilador, la verificación de exclusividad se eliminó por completo, como lo verificó una nueva ejecución del perfil.
- 19:12 - Pila contra montón
El ejemplo reemplaza el uso de 'Array' en la app con 'InlineArray', una colección de tamaño fijo almacenada en línea, que optimiza el uso de la memoria al eliminar el conteo de referencias y las verificaciones de exclusividad. Es ideal para el caché de píxeles ・・ una matriz de 64 elementos que nunca cambia de tamaño y se modifica en el lugar, lo que mejora el rendimiento sin la necesidad de copiar o compartir referencias.
- 21:08 - Recuento de referencias
En el ejemplo de optimización final de la app, el ejemplo utiliza los nuevos tipos "Span" para mejorar el rendimiento y mejorar la seguridad de la memoria. En Instrumentos, se utiliza el gráfico de llama del análisis del Perfilador de tiempo. Los datos perfilados se centran en 'QOIParser' y encuentran que se dedica un tiempo significativo a operaciones de conteo de referencias, particularmente con el tipo 'Datos' debido a su semántica de copia en escritura. 'Span' y sus tipos relacionados son una nueva forma de trabajar con memoria contigua en una colección. Utilizan la característica no escapable de Swift ('~Escapable'), que vincula sus duraciones de vida a la colección, lo que garantiza la seguridad de la memoria y elimina la necesidad de una gestión manual de la memoria. Esto permite un acceso eficiente a la memoria sin los riesgos asociados con los punteros inseguros. El ejemplo demuestra cómo utilizar los tipos 'Span' para reescribir métodos existentes, haciéndolos más simples, más seguros y con mayor rendimiento. En los métodos de análisis de imágenes, 'Datos' se reemplaza por 'RawSpan' y la sobrecarga del recuento de referencias se reduce enormemente. Además, se adopta 'OutputSpan' en el proceso de análisis para una mayor optimización, lo que hace que la operación de análisis sea seis veces más rápida que antes sin recurrir a punteros inseguros.
- 29:52 - Biblioteca de análisis binario Swift
Swift Binary Parsing le permite crear analizadores seguros y eficientes para formatos binarios. Ofrece herramientas para gestionar diversos aspectos de seguridad, incluida la prevención del desbordamiento de enteros, la especificación del signo, el ancho de bits y el orden de bytes y la validación de tipos personalizados. La biblioteca ya se encuentra en uso en Apple y está disponible públicamente para que puedas probarla y contribuir a través de los foros de Swift y GitHub.
- 31:03 - Próximos pasos
Las conclusiones clave incluyen: Uso de Xcode e instrumentos para crear perfiles de apps. Analizar el rendimiento de los algoritmos para identificar cuellos de botella. Explorando soluciones a lo anterior con los nuevos tipos 'InlineArray' y 'Span' introducidos en Swift 6.2.