-
Otimize o desempenho da CPU com o Instruments
Saiba como otimizar seu app para Apple Silicon com duas novas ferramentas assistidas por hardware no Instruments. Primeiro, vamos abordar como analisar o desempenho do seu app e, em seguida, vamos mostrar em detalhes cada função chamada com o instrumento Processor Trace. Também abordaremos como usar o instrumento CPU Counters para analisar seu código em busca de gargalos da CPU.
Capítulos
- 0:00 - Introdução
- 2:28 - Mentalidade de desempenho
- 8:50 - Ferramentas de análise de desempenho
- 13:20 - Span
- 14:05 - Processor Trace
- 19:51 - Análise de gargalos
- 31:33 - Resumo
- 32:13 - Próximas etapas
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
Vídeos relacionados
WWDC25
- Melhore o uso da memória e o desempenho com o Swift
- Monitore e otimize o consumo de energia do seu app
- Otimize o desempenho do SwiftUI com o Instruments
WWDC24
WWDC23
WWDC22
-
Buscar neste vídeo...
-
-
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 - Introdução
A otimização de código para CPUs com chip Apple Silicon é complexa devido às camadas de abstração entre o código-fonte do Swift e as instruções de máquina, bem como às maneiras complexas como as CPUs executam instruções fora de ordem e utilizam caches de memória. O Instruments ajuda os desenvolvedores a enfrentar essas complexidades e permite investigações de desempenho, além de analisar o desempenho do sistema para identificar o uso excessivo da CPU. Use os instrumentos Processor Trace e CPU Counters para registrar instruções, medir custos e analisar gargalos, resultando em códigos mais eficientes e melhor desempenho do app.
- 2:28 - Mentalidade de desempenho
Ao investigar problemas de desempenho dos apps, é essencial ser flexível e coletar dados para validar suposições. Lentidões podem ocorrer por vários motivos: threads bloqueadas aguardando recursos, APIs usadas indevidamente ou algoritmos ineficientes. Ferramentas como o CPU Gauge, no Xcode, e System Trace e Hangs instruments, no Instruments, são inestimáveis para identificar os padrões de uso da CPU, comportamentos de bloqueio e interface não responsiva. Antes de se aprofundar nas micro-otimizações, que podem dificultar a manutenção do código, é melhor explorar abordagens alternativas. Essas alternativas incluem evitar trabalho desnecessário, atrasar esse trabalho com concorrência, pré-calcular valores e armazenar em cache o estado calculado por operações complexas. Se essas estratégias forem esgotadas, a otimização do código vinculado à CPU se tornará necessária. Concentre-se no código que afeta muito a experiência do usuário, como o caminho crítico das interações do usuário. O ideal é adotar uma abordagem de otimização incremental, acompanhando o progresso com testes automatizados e métricas de desempenho no Xcode e no Instruments.
- 8:50 - Ferramentas de análise de desempenho
Para analisar o desempenho da CPU do exemplo de pesquisa binária apresentado nesta sessão, o Instruments conta com duas ferramentas de análise: o Time Profiler e o CPU Profiler. O Time Profiler faz amostragens periódicas da atividade da CPU, mas pode sofrer com aliasing, um efeito em que tarefas executadas periodicamente distorcem a representação do uso da CPU. O CPU Profiler, por outro lado, faz amostragens independentes de cada CPU com base na frequência do clock, o que o torna mais preciso e adequado para otimização de desempenho da CPU. Na análise, o instrumento CPU Profiler é escolhido e iniciado do navegador de teste do Xcode. Em seguida, a gravação no Instruments é definida como Modo Adiado para minimizar a sobrecarga. São apresentadas as áreas dentro do Instruments, entre elas, a visualização da linha do tempo, suas trilhas e faixas e a visualização detalhada dos resultados perfilados. Ao examinar as faixas Points of Interest e Process no processo "xctest", foi identificada a região específica em que as pesquisas binárias ocorrem no app de exemplo. A árvore de chamadas na visualização dos detalhes mostra que as funções relacionadas ao protocolo Collection consomem um tempo significativo da CPU. Para otimizar o desempenho, é recomendável alternar para um tipo de contêiner mais eficiente, como Span, para evitar sobrecargas associadas a Array com semântica de cópia na gravação e ao uso de genéricos.
- 13:20 - Span
O Swift 6.2 introduz o Span, uma estrutura de dados otimizada no uso de memória que representa um intervalo contíguo de memória com endereço base e contagem. Usar Span para tipos de entrada e saída de pesquisa binária melhora o desempenho em 400%, sem alterar o algoritmo. Em seguida, para otimizar ainda mais o desempenho, o instrumento Processor Trace é utilizado para investigar a sobrecarga causada pela verificação de limites.
- 14:05 - Processor Trace
O Instruments 16.3 introduziu um novo e importante instrumento: o Processor Trace. Essa ferramenta permite capturar um rastreamento abrangente de todas as instruções executadas pelo processo do app no espaço do usuário no Mac e iPad Pro com chip M4 ou posterior ou no iPhone com chip A18 ou posterior. O Processor Trace requer a ativação de determinados ajustes no dispositivo e é mais eficiente quando usado em sessões curtas de rastreamento, já que pode gerar uma quantidade significativa de dados. Ao registrar cada decisão de ramificação, contagem de ciclos e horário atual, o Instruments reconstrói o caminho exato de execução do app. Os dados são apresentados visualmente em um gráfico de chama, que mostra o tempo consumido por chamada de função ao longo do tempo. Diferentemente dos gráficos de chama tradicionais, que usam amostragem, o gráfico do Processor Trace fornece uma representação exata de como a CPU executou o código. Isso permite identificar gargalos de desempenho com uma precisão inédita. A análise dos dados de rastreamento revela que as sobrecargas de metadados de protocolos e a impossibilidade de aplicar inlining em comparações numéricas estão causando lentidão significativa em uma função específica de busca binária. Para resolver essa questão, a função é especializada manualmente para o tipo Int, resultando em uma melhoria substancial de desempenho de cerca de 170%. No entanto, é necessário otimizar ainda mais porque a implementação da busca binária do app continua contribuindo para a lentidão geral.
- 19:51 - Análise de gargalos
As CPUs com chip Apple Silicon executam instruções em duas fases: a Entrega de Instruções e o Processamento de Instruções. Ambas as fases são executadas em pipeline para permitir o paralelismo no nível de instrução. Isso permite que várias operações sejam processadas simultaneamente, maximizando a eficiência. No entanto, gargalos podem ocorrer no pipeline, interrompendo as operações e limitando o paralelismo. O instrumento CPU Counters ajuda a identificar esses gargalos ao contar eventos em cada unidade de CPU. Ele usa modos predefinidos para medir o desempenho da CPU e dividir o trabalho em categorias amplas. Ao analisar os dados amostrados, é possível identificar instruções específicas que causam problemas, como direções de ramificação previstas incorretamente, o que pode causar o desperdício de ciclos e a degradação do desempenho. As CPUs executam instruções fora de ordem usando preditores de ramificação para melhorar o desempenho. No entanto, ramificações aleatórias podem enganar esses preditores. Como mitigação, o código é reescrito para evitar ramificações difíceis de prever, resultando em uma busca binária sem ramificações que é cerca de duas vezes mais rápida. O foco da otimização do app, então, muda para o acesso à memória, já que as CPUs utilizam uma hierarquia de caches para acelerar a recuperação de dados. O padrão de acesso do algoritmo de busca binária foi patológico para essa hierarquia, causando perdas frequentes de cache. Ao reorganizar os elementos da matriz usando um layout Eytzinger, a localidade do cache melhorou, e a busca binária ficou 200% mais rápida. Apesar dessas otimizações significativas, o código ainda enfrenta gargalo técnico no processamento de instruções. No entanto, a função de busca como um todo ficou cerca de 2.500% mais rápida, graças a diversas técnicas de profiling e micro-otimização.
- 31:33 - Resumo
Ao medir e otimizar primeiro as sobrecargas de software e, então, focar nos gargalos da CPU, o desempenho do app de busca binária melhorou, resolvendo questões facilmente negligenciadas e alcançando melhor alinhamento à arquitetura da CPU.
- 32:13 - Próximas etapas
Para otimizar apps, use o Instruments para coletar dados, executar testes de desempenho e analisar resultados. Também é possível conferir as sessões sobre o desempenho do Swift e ler o "Guia de Otimização para CPUs com chip Apple Silicon" na documentação para desenvolvedores. Faça perguntas ou envie seu feedback nos Fóruns para desenvolvedores.