-
Optimiza el rendimiento del CPU con instrumentos
Aprende a optimizar tu app para el chip de Apple con dos herramientas asistidas por hardware en instrumentos. Comenzaremos con cómo crear un perfil de tu app y profundizaremos mostrando cada función convocada con Processor Trace. Analizaremos cómo usar los modos de los contadores para buscar en tu código cuellos de botella de CPU.
Capítulos
- 0:00 - Introducción y agenda
- 2:28 - Mentalidad de rendimiento
- 8:50 - Perfiladores
- 13:20 - Span
- 14:05 - Seguimiento del procesador
- 19:51 - Análisis de cuellos de botella
- 31:33 - Resumen
- 32:13 - Próximos pasos
Recursos
- 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
Videos relacionados
WWDC25
- Crear perfiles y optimizar el uso de energía en tu app
- Mejora el uso y el rendimiento de la memoria con Swift
- Optimiza el rendimiento de SwiftUI con instrumentos
WWDC24
WWDC23
WWDC22
-
Buscar este video…
-
-
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 - Introducción y agenda
Optimizar el código para los CPU con Apple Chip es complejo debido a las capas de abstracción entre el código fuente Swift y las instrucciones de la máquina, así como las formas complejas en que los CPU ejecutan instrucciones fuera de orden y usan cachés de memoria. Instruments ayuda a los desarrolladores a navegar por estas complejidades y permite realizar investigaciones de rendimiento y perfiles del rendimiento del sistema para identificar el uso excesivo del CPU. Usa los instrumentos Processor Trace y CPU Counters para registrar instrucciones, medir costos y analizar cuellos de botella, lo que conduce a un código más eficiente y a un mejor rendimiento de la app.
- 2:28 - Mentalidad de rendimiento
Al investigar problemas de rendimiento en tus apps, es fundamental mantener una mente abierta y recopilar datos para validar las suposiciones. Las ralentizaciones pueden deberse a diversos factores, como subprocesos bloqueados que esperan recursos, API mal utilizadas o algoritmos ineficientes. Herramientas como CPU Gauge en Xcode, así como los instrumentos System Trace y Hangs en Instruments, son invaluables para identificar patrones de uso de CPU, comportamientos de bloqueo y falta de respuesta de la UI. Antes de empezar con las microoptimizaciones, que pueden complicar el mantenimiento del código, es mejor explorar enfoques alternativos. Estas alternativas incluyen evitar trabajo innecesario, retrasar ese trabajo con concurrencia, precalcular valores y almacenar en caché el estado calculado con operaciones complejas. Si se agotan estas estrategias, hay que optimizar el código limitado por la CPU. Enfócate en el código que tiene un impacto significativo en la experiencia del usuario, como la ruta crítica de las interacciones del usuario. Se recomienda la optimización incremental, con el progreso medido con pruebas automatizadas y métricas de rendimiento en Xcode y en Instruments.
- 8:50 - Perfiladores
Para analizar el rendimiento del CPU del ejemplo de búsqueda binaria en esta sesión, hay dos perfiladores disponibles en Instruments: Time Profiler y CPU Profiler. Time Profiler muestrea periódicamente la actividad del CPU pero puede sufrir aliasing, donde el trabajo periódico distorsiona la representación del uso del CPU. CPU Profiler, en cambio, muestrea los CPU de forma independiente según su frecuencia de reloj, lo que lo hace más preciso y adecuado para la optimización del CPU. Para este análisis, se elige CPU Profiler y se ejecuta desde el navegador de pruebas de Xcode, luego el registro en Instruments se configura en Modo Diferido para minimizar la sobrecarga. Se presentan las áreas dentro de Instruments, como la vista de la línea de tiempo, sus pistas y carriles, y la vista de detalle que muestra los resultados perfilados. Al examinar la pista Puntos de interés y la pista Proceso para el proceso "xctest", se identifica la región específica donde ocurren búsquedas binarias en la app de ejemplo. El árbol de llamadas en la vista detallada muestra que las funciones relacionadas con el protocolo "Collection" consumen un tiempo de CPU significativo. Para optimizar el rendimiento, se sugiere cambiar a un tipo de contenedor más eficiente, como "Span", para evitar sobrecargas asociadas con "Array", que tiene semántica de copia en escritura y genéricos.
- 13:20 - Span
Swift 6.2 presenta 'Span', una estructura de datos de memoria eficiente que representa un rango de memoria contigua con una dirección base y un recuento. El uso de 'Span' para los tipos de entrada y salida de búsqueda binaria mejora el rendimiento en un 400% sin alterar el algoritmo. Luego, para optimizar aún más el rendimiento, se usa el instrumento Processor Trace para investigar la sobrecarga de comprobación de límites.
- 14:05 - Seguimiento del procesador
Instruments 16.3 introdujo un nuevo instrumento importante llamado Processor Trace. Esta herramienta te permite capturar un rastro completo de todas las instrucciones ejecutadas por el proceso de tu app en el espacio del usuario en Mac y iPad Pro con chips M4 y posteriores, o iPhone con chips A18 y posteriores. Processor Trace requiere que se habiliten configuraciones específicas del dispositivo y es más efectivo cuando se usa para sesiones de rastreo cortas, ya que puede generar cantidades sustanciales de datos. Al registrar cada decisión de ramificación, el recuento de ciclos y el tiempo actual, Instruments reconstruye la ruta de ejecución exacta de la app. Los datos se presentan visualmente en un gráfico de llama, que muestra el tiempo que tarda cada ejecución de función con el tiempo. A diferencia de los gráficos de llama tradicionales que usan muestreo, el gráfico de llama de Processor Trace ofrece una representación exacta de cómo el CPU ejecutó el código. Esto te permite identificar cuellos de botella en el rendimiento con una precisión sin precedentes. A través del análisis de los datos de rastreo, es evidente que las sobrecargas de metadatos del protocolo y la imposibilidad de realizar comparaciones de números en línea están causando ralentizaciones significativas en una función de búsqueda binaria específica. Para solucionar esto, la función se especializa manualmente para el tipo Int, lo que genera una mejora sustancial del rendimiento de alrededor del 170%. Sin embargo, aún se necesita una mayor optimización porque la implementación de búsqueda binaria de la app continúa contribuyendo a las ralentizaciones generales de la app.
- 19:51 - Análisis de cuellos de botella
Los CPU con Apple Chip ejecutan instrucciones en dos fases: Entrega y procesamiento de instrucciones, que se canalizan para permitir el paralelismo a nivel de instrucción. Esto permite procesar múltiples operaciones en simultaneo, maximizando la eficiencia. Sin embargo, pueden producirse cuellos de botella en la canalización, lo que puede paralizar las operaciones y limitar el paralelismo. El instrumento CPU Counters ayuda a identificar estos cuellos de botella al contar eventos en cada unidad de CPU. Usa modos preestablecidos para medir el rendimiento del CPU y dividir el trabajo en categorías amplias. Al analizar los datos muestreados, se pueden identificar instrucciones específicas que causan problemas, como direcciones de ramificación mal predichas, que pueden generar ciclos desperdiciados y degradación del rendimiento. Los CPU ejecutan instrucciones fuera de orden utilizando predictores de ramificaciones para mejorar el rendimiento. Sin embargo, las ramas aleatorias pueden inducir a error a estos predictores. Para mitigarlo, el código se reescribe para evitar ramificaciones difíciles de predecir, lo que resulta en una búsqueda binaria sin ramificaciones que es cerca del doble de rápida. El foco de la optimización de la app se desplaza entonces al acceso a la memoria, ya que los CPU utilizan una jerarquía de cachés para acelerar la recuperación de datos. El patrón de acceso del algoritmo de búsqueda binaria era patológico para esta jerarquía, lo que provocaba frecuentes fallos de caché. Al reorganizar los elementos de la matriz con un diseño de Eytzinger, se mejora la localidad de la caché y la búsqueda binaria se vuelve un 200% más rápida. A pesar de estas optimizaciones significativas, el código aún tiene cuellos de botella técnicos en el procesamiento de instrucciones, pero la función de búsqueda general se volvió cerca de un 2500% más rápida mediante varias técnicas de creación de perfiles y microoptimización.
- 31:33 - Resumen
Al medir y optimizar primero los costos generales del software y luego concentrarse en los cuellos de botella del CPU, se mejoró el rendimiento de la app de búsqueda binaria al resolver problemas que se pasan por alto con facilidad y adaptarse mejor a la arquitectura del CPU.
- 32:13 - Próximos pasos
Para optimizar las apps, usa Instruments para recopilar datos, ejecutar pruebas de rendimiento y analizar resultados. También puedes ver sesiones sobre el rendimiento de Swift y leer la "Guía de optimización de CPU con Apple Chip" en la documentación para desarrolladores. Haz preguntas o comentarios en los foros para desarrolladores.