View in English

  • Global Nav Open Menu Global Nav Close Menu
  • Apple Developer
Search
Cancel
  • Apple Developer
  • News
  • Discover
  • Design
  • Develop
  • Distribute
  • Support
  • Account
Only search within “”

Quick Links

5 Quick Links

Vídeos

Abrir menu Fechar menu
  • Coleções
  • Tópicos
  • Todos os vídeos
  • Sobre

Mais vídeos

  • Sobre
  • Resumo
  • Transcrição
  • Código
  • Melhore o uso da memória e o desempenho com o Swift

    Descubra maneiras de melhorar o desempenho e o gerenciamento de memória do seu código Swift. Vamos explorar maneiras de aprimorar seu código, desde alterações algorítmicas avançadas até o uso dos novos tipos de InlineArray e Span, que oferecem um controle mais preciso sobre a memória e as alocações.

    Capítulos

    • 0:00 - Introdução
    • 1:19 - App: Formato QOI e parser
    • 2:25 - Algoritmos
    • 8:17 - Alocações
    • 16:30 - Exclusividade
    • 19:12 - Pilha x heap
    • 21:08 - Contagem de referências
    • 29:52 - Biblioteca de análise binária do Swift
    • 31:03 - Próximas etapas

    Recursos

    • Performance and metrics
    • Swift Binary Parsing
    • The Swift Programming Language
    • The Swift website
      • Vídeo HD
      • Vídeo SD

    Vídeos relacionados

    WWDC25

    • Monitore e otimize o consumo de energia do seu app
    • Novidades do Swift
    • Otimize o desempenho da CPU com o Instruments

    WWDC24

    • Explore Swift performance
  • Buscar neste vídeo...

    Olá! Sou Nate Cook e trabalho na equipe Swift Standard Library. Hoje vamos ver como entender e melhorar o desempenho do seu código usando as novidades da linguagem e da Biblioteca Padrão no Swift 6.2. Vamos usar os novos tipos InlineArray e Span, experimentar valores genéricos e aprender sobre tipos de escopo restrito. Vamos usar todas essas novas ferramentas para eliminar retenções e lançamentos, verificações de exclusividade e outros trabalhos extras. Também vou apresentar uma nova biblioteca de código aberto que usa todas essas ferramentas para tornar a criação de analisadores binários rápida e segura. Chama-se Análise binária do Swift. A biblioteca tem por foco a velocidade e fornece ferramentas para gerenciar vários tipos de segurança. Queremos códigos rápidos, e o Swift oferece as ferramentas necessárias para isso acontecer. Mas, às vezes, as coisas não são tão rápidas quanto esperamos. Nesta sessão, vamos descobrir na prática onde nosso código está gastando tempo e experimentar vários tipos de otimização de desempenho... Como escolher os algoritmos certos. Como evitar alocações extras. Como eliminar verificações de exclusividade. Como mover alocações do heap para pilha. E como reduzir a contagem de referências. Na nossa jornada, vamos analisar pequeno app que criei. Ele é um visualizador para um formato de imagem chamado QOI e inclui um analisador criado manualmente para o formato. QOI é um formato de imagem sem perdas, simples o suficiente para que sua especificação caiba em uma única página, o que o torna útil para experimentar diferentes abordagens e ver seu desempenho. O formato QOI usa um idioma padrão para formatos binários, com um cabeçalho de tamanho fixo seguido por uma seção de dados, que inclui um número dinâmico de pixels codificados de tamanhos diferentes. Os pixels codificados assumem várias formas. Um pixel pode ser um valor RGB ou RGBA, uma diferença relativa ao pixel anterior, uma pesquisa em um cache de pixels vistos anteriormente ou... apenas o número de vezes que o pixel anterior será repetido. Vamos experimentar meu app de análise de QOI.

    Posso abrir este arquivo de ícone, que tem apenas alguns kilobytes, e ele carrega imediatamente.

    Esta foto de um pássaro é um pouco maior. Pode levar alguns segundos para carregar... E aí está. O que está demorando tanto? Quando há uma lentidão perceptível ao trabalhar com dados do mundo real, geralmente é um sinal de uso incorreto de um algoritmo ou estrutura de dados. Vamos usar o Instruments para encontrar e abordar a origem desse problema. Na minha biblioteca de análise, criei um teste que analisa a mesma imagem de pássaro que demorou a carregar. Posso clicar no botão Executar para executar o teste...

    E ele termina em questão de alguns segundos. Além de usar o teste para verificar se está tudo certo, também posso traçar o perfil do teste em Instruments para ver o desempenho. Desta vez, vou clicar com o botão direito em Executar. Há uma opção no menu para criar o perfil do teste.

    Eu adoro esse recurso. Ao criar o perfil de teste, posso focar na parte específica do código em que tenho interesse. Vou selecionar essa opção agora para iniciar o Instruments.

    O Instruments abre no seletor de modelos, mostrando diversas maneiras de ajudar você a entender o desempenho do código. Vamos usar dois instrumentos diferentes hoje, então vou começar com o modelo em branco.

    Posso adicionar instrumentos clicando no botão Add instrument. Vou adicionar o instrumento Allocations para entender como meu analisador está alocando memória. Como quero saber onde meu app está gastando tempo, vou adicionar o instrumento Time Profiler.

    O Time Profiler é um ótimo ponto de partida para questões de desempenho. Vamos ocultar a barra lateral para dar um pouco mais de espaço para os resultados. Depois, use o botão Record para iniciar o teste.

    Podemos ver alguns resultados na janela.

    Os instrumentos incluídos no perfil ficam na parte superior da janela. Vamos usar o "Time Profiler" primeiro, então vou mantê-lo selecionado. Na parte inferior fica a visualização de detalhes da ferramenta selecionada. Do lado esquerdo, há uma lista de chamadas capturadas... À direita fica o rastreamento de pilhas mais pesado para a chamada atualmente selecionada.

    Quero ver as chamadas capturadas com mais frequência primeiro, independentemente de como são alcançadas, então vou clicar no botão Call Tree e, em seguida, marcar a caixa de seleção Invert Call Tree. Posso alternar para uma visualização gráfica usando esse botão na parte superior da visualização de detalhes. Quando clico, a visualização muda para mostrar o perfil como um gráfico de chama.

    Cada barra no gráfico de chama mostra a proporção das vezes que uma chamada foi capturada durante o perfil. Neste caso, há uma enorme barra dominando o processo, chamada platform_memmove. Esse mesmo símbolo também aparece no rastreamento de pilha. memmove é uma chamada do sistema para copiar dados, então essa barra enorme indica que o analisador está passando a maior parte do tempo copiando dados, em vez de apenas lê-los. Mas isso não deveria acontecer. Vamos descobrir qual parte do meu código está levando às cópias. Quero ver todos os quadros no rastreamento de pilha, então vou clicar no botão Show all frames na parte superior da exibição.

    Na parte superior do rastreamento estão as chamadas do sistema, inclusive platform_memmove e algumas versões especializadas de métodos fornecidos pelo tipo Foundation Data. Você já deve ter visto métodos especializados como esses em um rastreamento de pilha ou durante a depuração. Esses métodos especializados são versões específicas de tipo de código genérico que o compilador do Swift gera para você.

    Finalmente, chegamos ao método que defini, readByte.

    Como é a função mais próxima do problema no código, é o lugar certo para começar. Para acessar o método, posso clicar com o botão direito e escolher Reveal in Xcode. Aqui está a declaração do método readByte no Xcode. O Instruments me direcionou para esta linha, onde vou soltar o primeiro byte e chamar o inicializador de dados. Com o Instruments, consegui identificar todas essas chamadas de memmove como uma fonte potencial de lentidão na biblioteca e ir diretamente para a linha específica de código que está causando as cópias excessivas.

    Este método auxiliar é realmente importante... porque meu código de análise chama readByte repetidamente enquanto consome os dados binários brutos.

    Pensei que isso apenas reduziria os dados, retornando o primeiro byte e movendo o início dos dados para frente toda vez que chamasse o método readByte. Porém, toda vez que leio um byte, ele copia todo o conteúdo dos dados em uma nova alocação. É muito mais trabalho do que eu esperava. Vamos corrigir esse erro. Vou voltar ao Xcode e editar o método readByte.

    Como o tipo de dados é projetado para de modo a diminuir ambas as extremidades, temos acesso a um método de coleta chamado popFirst(). O popFirst() retorna o primeiro byte em data e, então, desliza a frente da coleção para frente, reduzindo-a em um byte. Exatamente o que queremos.

    Com essa correção, posso voltar ao meu teste e executar o perfil novamente.

    O Instruments abre automaticamente, com o teste já em execução e a mesma configuração de perfil. Excelente! A enorme barra de platform_memmove desapareceu do gráfico de chama.

    Quando comparo meu código, também noto uma velocidade muito maior resultante da alteração. Isso é fantástico. Porém, com uma alteração algorítmica desse tipo, a mudança absoluta não é tudo. Na minha versão original, a relação entre o tamanho da imagem e o tempo que levava para analisar era quadrática. À medida que as imagens analisadas aumentavam, o tempo de análise ficava drasticamente maior. Após a correção das cópias, a relação agora é linear. Há mais correspondência direta entre o tamanho da imagem e o tempo que leva para analisar. Temos mais muitas melhorias em vista que vão aprimorar o desempenho linear e poderemos comparar essas melhorias de modo mais direto.

    Com essa questão resolvida, vamos conferir outra armadilha comum no desempenho: alocações extras. Vejamos qual é o rastreamento de pilha mais pesado no momento. Essas chamadas mostram que há muito tráfego para os métodos que alocam e desalocam uma matriz do Swift. Alocar e realocar memória pode ser caro. Meu analisador será mais rápido se eu descobrir de onde as alocações extras estão vindo e eliminá-las. Para ver as alocações que o analisador está fazendo, posso usar o instrumento Allocations que adicionamos anteriormente. Há diversos elementos que indicam que meu código provavelmente está causando alocações desnecessárias. O primeiro é o grande número: quase um milhão de alocações para analisar uma imagem. Acho que isso pode melhorar. Em segundo lugar, podemos ver que quase todas essas alocações são transitórias, marcadas como de curta duração pelo instrumento Allocations. Para encontrar a origem do problema, vou usar o painel de detalhes na exibição de árvore de chamadas. Primeiro, clico no botão pop-up chamado Statistics. Depois, escolho Call Trees.

    Com o thread superior selecionado, vou examinar o rastreamento de pilha para encontrar a parte do meu código mais próxima do problema. Como esse rastreamento de pilha não é invertido, preciso começar a olhar para a parte inferior do rastreamento. O primeiro símbolo do meu analisador é o método RGBAPixel.data.

    Quando clico nesse método, ele aparece na janela de detalhes da árvore de chamadas. Quando clico com o botão direito do mouse no método, posso escolher Reveal in Xcode para ir diretamente para o código-fonte.

    Esse método parece ser a fonte das alocações extras. Posso ver que toda vez que ele é chamado, retorna uma matriz com os valores RGB ou RGBA do pixel. Isso significa que ele criará uma matriz e alocará espaço para pelo menos três elementos toda vez que for chamado.

    Para descobrir onde ele está sendo usado, vou clicar com o botão direito do mouse no nome da função e escolher Show callers. O chamador é esse fechamento na função de análise principal, que é apenas uma parte da grande cadeia de prefixos e flatMap. Para entender por que o código está fazendo tantas alocações separadas, vamos ver um passo a passo de como elas se acumulam.

    Primeiro, o método readEncodedPixels analisa os dados binários em pixels codificados, que são os tipos diferentes de pixel que mencionei anteriormente, e precisa alocar espaço suficiente para armazená-los.

    Depois, decodePixels é chamado para cada pixel codificado para produzir um ou mais pixels RGBA. A maioria das codificações se transforma em um único pixel, mas há uma codificação que diz que precisamos repetir o pixel anterior algumas vezes. Para isso, decodePixels sempre retorna uma matriz. Cada matriz precisa ser alocada.

    A parte de “achatamento” do flatMap pega todas as matrizes que acabamos de criar e as combina em uma matriz muito maior. Essa é a nova alocação, e a alocação das matrizes que acabamos de criar é desfeita.

    Esse método de prefixo coloca um limite no número de pixels que podemos produzir.

    O segundo flatMap começa chamando RGBAPixel.data, o método que sinalizamos quando usamos o instrumento Allocations. Vimos antes que ele retorna uma matriz com três ou quatro elementos. O que vemos agora significa que uma dessas matrizes de 3 ou 4 elementos é criada para cada pixel na imagem final. Às vezes, o compilador pode otimizar algumas alocações extras, mas, como vimos no rastreamento, isso nem sempre acontece.

    Agora, as matrizes são nivelados mais uma vez em uma nova matriz grande. Por fim, essa grande matriz de dados de pixel RGB ou RGBA é copiada em uma nova instância Data para que possa ser retornada.

    Essas linhas de código são elegantes. Elas empenham muita energia em algumas chamadas de método curtas e encadeadas. Mas ser mais curto não significa ser mais rápido. Em vez de trabalhar em todas essas diferentes etapas e, eventualmente, terminar com uma instância de dados para retornar, e se alocarmos os dados primeiro e, depois, gravarmos cada pixel à medida que decodificamos os dados de origem binários? Assim, podemos fazer todo o mesmo processamento sem precisar dessas alocações intermediárias. Voltei à função de análise. Vamos gravar novamente o método para eliminar as alocações extras.

    A primeira coisa que vamos fazer é calcular totalBytes: o tamanho final dos dados do resultado. Então, vamos alocar pixelData com a quantidade certa de armazenamento. A variável offset controla a quantidade de dados que gravamos. Essa alocação inicial significa que não teremos que fazer outras alocações à medida que avançamos pelos dados binários.

    Em seguida, vamos analisar cada parte dos dados e processá-los imediatamente. Podemos usar uma instrução switch para lidar com o pixel analisado.

    Para os pixels codificados que indicam uma execução, faremos um loop quantas vezes que for necessário, gravando os dados de pixel a cada vez.

    Para qualquer outro tipo de pixel, vamos decodificar e gravar diretamente nos dados. Essa é a nova gravação completa, sem alocações além dos dados que precisamos retornar. Vamos verificar se corrigimos esse problema criando o perfil do teste novamente.

    Podemos ver de imediato que o número de alocações é muito menor. Para ver o número real de alocações no código, posso usar o filtro. Vou clicar no campo de filtro na parte inferior da janela. Digite QOI.init para filtrar qualquer árvore de chamadas que não inclua QOI.init no rastreamento de pilha. As linhas restantes mostram que agora nosso código analisador faz apenas algumas alocações, totalizando pouco menos de dois megabytes. Quando mantenho pressionada a opção e clico no triângulo de abertura, a árvore de chamadas se expande.

    A árvore expandida mostra o que queremos.

    A única coisa que estamos alocando são os dados que armazenam a imagem resultante.

    Tendo em vista os benchmarks, essa é outra grande melhoria. Ao cortar essas alocações extras, reduzimos o tempo de execução em mais da metade.

    Até agora, fizemos duas alterações algorítmicas no analisador, eliminando o excesso de cópias acidentais e reduzindo o número de alocações. Para nossas próximas melhorias, vamos usar algumas técnicas mais avançadas para permitir que o compilador Swift elimine muito do trabalho de gerenciamento automático de memória que acontece no tempo de execução.

    Primeiro, vamos falar sobre como funcionam as matrizes e outros tipos de coleção. O tipo de matriz do Swift é uma das ferramentas mais comuns em nossa caixa por ser rápido, seguro e prático. As matrizes podem aumentar ou diminuir, então você não precisa saber com antecedência com quantos itens vai trabalhar. O Swift cuida da memória para você nos bastidores. As matrizes também são tipos de valor, o que significa que as alterações na cópia de uma matriz não afetam outras cópias. Se você fizer uma cópia de uma matriz, atribuindo-a a uma variável diferente ou passando-a para uma função, o Swift não duplicará imediatamente os elementos. Em vez disso, ele usará uma otimização chamada de cópia na gravação, que atrasa a duplicação até que você realmente altere uma das matrizes.

    Esses recursos tornam as matrizes uma ótima coleção de uso geral, mas também têm algumas compensações. Para dar conta de seu tamanho dinâmico e várias referências, a matriz armazena seu conteúdo em uma alocação separada, geralmente no heap. O tempo de execução do Swift usa a contagem de referência para controlar o número de cópias de cada matriz e, quando você faz uma alteração, as matrizes verificam a exclusividade para ver se precisam copiar seus elementos. Finalmente, para garantir que o código permaneça seguro, o Swift impõe exclusividade, o que significa que dois elementos diferentes não podem modificar os mesmos dados ao mesmo tempo. Embora essa regra seja frequentemente imposta no tempo de compilação, às vezes, ela só pode ser aplicada no tempo de execução. Agora que aprendemos esses conceitos de entrada, vamos ver como eles aparecem na criação de perfil. Vamos começar procurando verificações de exclusividade de tempo de execução, que podem aumentar o trabalho do programa e atrapalhar as otimizações. Antes de começar a procurar verificações de exclusividade, temos um problema. Melhoramos nosso desempenho o suficiente para que o Instruments não tenha tempo suficiente para inspecionar o processo de análise. Podemos fornecer mais itens para analisar percorrendo o código de análise umas 50 vezes para ter o resultado desejado.

    Vamos ver esse perfil mais avançado.

    Os testes de exclusividade aparecem em um rastreamento como os símbolos swift_beginAccess e swiftendAccess. Novamente, vou clicar na caixa de filtro na parte inferior da janela. Em seguida, digite o nome do símbolo.

    Na parte superior do gráfico de chama, swift_beginAccess aparece algumas vezes, com os símbolos que exigem essa verificação logo abaixo. Esses símbolos são os métodos de acesso do pixel anterior e do cache de pixels, que são armazenados na classe State do analisador. Vou voltar para o Xcode e encontrar essa declaração. Aqui está... State é uma classe com essas duas propriedades que vimos no gráfico de chama. Modificar uma instância de classe é uma das situações em que o Swift precisa verificar a exclusividade no tempo de execução, então essa declaração é o motivo pela qual vemos o que estamos vendo. Podemos eliminar essa verificação retirando as propriedades da classe e colocando-as diretamente no tipo de analisador.

    Em seguida, vamos localizar e remover acessos de state para previousPixel e pixelCache.

    Durante a compilação, o compilador indica que há mais de trabalho a fazer.

    Como as propriedades de estado não estão mais aninhadas em uma classe, não posso modificá-las em um método não mutante.

    Vou aceitar essa correção para tornar o método mutante.

    Há mais um para corrigir...

    E está pronto. Com essa mudança, voltemos ao teste.

    Vamos também gravar de novo um perfil para ver a mudança.

    Vou filtrar swift_beginAccess novamente.

    Não há nada aqui. Removemos completamente a verificação de exclusividade do tempo de execução. Vamos dar outra olhada nessas variáveis de estado. Aqui é um bom lugar para usar um novo recurso do Swift para mover dados da memória de heap para a memória de pilha e garantir que as verificações de exclusividade não voltem a acontecer. O cache de pixels no analisador é uma matriz de RGBAPixels. Ele é inicializado com 64 elementos e nunca muda de tamanho. Esse cache seria um ótimo lugar para usar o novo tipo InlineArray. InlineArray é um novo tipo de biblioteca padrão no Swift 6.2. Como uma matriz normal, ele armazena vários elementos do mesmo tipo na memória contígua, mas tem algumas diferenças importantes. Primeiro, as matrizes em linha têm um tamanho fixo definido no tempo de compilação. Ao contrário das matrizes comuns que você pode acrescentar ou remover, InlineArray usa o novo recurso de valores genéricos para tornar o tamanho uma parte do tipo. Isso significa que, embora seja possível fazer alterações nos elementos de uma matriz em linha, não é possível acrescentar, remover ou atribuir uma matriz em linha a outra de tamanho diferente.

    Em segundo lugar, como o nome indica, quando você usa InlineArray, os elementos são sempre armazenados em linha em vez de em uma alocação separada. As matrizes em linha não compartilham armazenamento entre cópias e não usam o recurso de cópia na gravação. Em vez disso, eles são copiados sempre que você faz uma cópia. Isso elimina a necessidade de contagem de referência e verificações de exclusividade que as matrizes normais exigem. Esse comportamento de cópia diferente do InlineArray é uma faca de dois gumes: se for necessário fazer cópias ou compartilhar referências entre diferentes variáveis ou classes para poder usar a matriz, o InlineArray poderá não ser a opção certa. Nesse caso, no entanto, o cache de pixels é uma matriz de tamanho fixo modificada quando implementada, mas nunca copiada. Um lugar perfeito para usar InlineArray.

    Para nossa otimização final, vamos usar os novos tipos de trecho da biblioteca padrão para eliminar a maior parte da contagem de referência durante a análise. De volta ao gráfico de chama do perfil de tempo, vamos usar a filtragem novamente para ver apenas o analisador QOI. Vou adicionar QOI.init na caixa de filtro.

    A exibição muda para se concentrar só nos rastreamentos de pilha que incluem o inicializador de análise. Vamos procurar os símbolos de retenção e liberação. swift_retain é essa barra rosa, aparecendo em 7% das amostras, e swift_release é essa que aparece nos outros 7%. A verificação de exclusividade de que falamos antes também aparece aqui, nos outros 3% das amostras.

    Para descobrir de onde estão vindo, vou clicar novamente em swift_release e, como fizemos antes, examinar o rastreamento de pilha mais pesado para encontrar o primeiro método definido pelo usuário. Parece que é o mesmo método readByte com o qual começamos.

    Desta vez, não estamos lidando com uma questão algorítmica, mas com o uso de dados em si. Assim como a matriz, os dados geralmente armazenam sua memória no heap e precisam ser contados como referência. Essas operações de contagem de referência (retenção e liberação) são eficientes, mas podem exigir muito tempo quando acontecem em um ciclo apertado, assim como esse método. Para lidar com isso, queremos passar do trabalho com um tipo de coleção de alto nível, como dados ou matriz, para um tipo que não cause essa explosão de contagem de referência. Até o Swift 6.2, usava-se um método como withUnsafeBufferPointer para acessar o armazenamento subjacente de uma coleção. Esses métodos permitem gerenciar manualmente a memória, sem contagem de referências, mas deixam o código sem segurança.

    Vale a pena perguntar: por que os ponteiros não são seguros? O Swift os indica como não seguros porque eles ignoram muitas das garantias de segurança da linguagem. Eles podem apontar para memória inicializada e não inicializada, descartam algumas garantias de tipo e podem escapar do contexto, levando a um risco de acesso à memória que não está mais alocada. Ao usar ponteiros não seguros, você fica totalmente responsável por manter a segurança do código intacta. O compilador não vai ajudar. A função processUsingBuffer usa ponteiros não seguros corretamente. O uso fica inteiramente no fechamento do ponteiro do buffer não seguro, e somente o resultado do cálculo é retornado no final. Por outro lado, a função getPointerToBytes() é perigosa. Ela contém dois erros de programação principais. A função cria uma matriz de bytes e chama o método com UnsafeBufferPointer, mas, em vez de limitar o uso do ponteiro ao fechamento, ela retorna o ponteiro para o escopo externo. Erro número 1. Pior ainda, o código retorna esse ponteiro que não é mais válido da própria função. Erro número 2. Os dois erros estendem a vida útil do ponteiro para além do tempo que ele está apontando, criando uma referência perigosa remanescente para a memória movida ou desalocada. Para ajudar, o Swift 6.2 apresenta um novo grupo de tipos chamado Spans. Spans são uma nova maneira de trabalhar com a memória contígua pertencente a uma coleção. Importante: os Spans usam o novo recurso de linguagem de escopo restrito, que permite que o compilador vincule sua vida útil à coleção que as fornece. A memória à qual um Span dá acesso tem uma garantia de vida útil igual à do Span, sem chance de uma referência persistente. Como cada tipo de Span é declarado como tendo escopo restrito, o compilador impede que você escape ou retorne um Span fora do contexto em que o recuperou.

    Esse método processUsingSpan mostra como você pode usar um Span para gravar códigos mais simples e seguros do que os ponteiros permitem. Para obter um Span sobre os elementos da matriz, basta usar a propriedade Span. Sem usar um fechamento, temos acesso ao armazenamento da matriz, com a mesma eficiência de ponteiros não seguros, mas sem a falta de segurança. Podemos ver o recurso de linguagem de escopo restrito se tentamos gravar novamente a função perigosa de antes. A primeira coisa que vamos descobrir é que não é possível gravar a mesma assinatura de função com Span. Como a vida útil de um Span está vinculada à coleção que o fornece, sem transmitir uma coleção ou extensão, não temos onde obter uma vida útil para o Span que está sendo distribuído.

    E se tentarmos ocultar o Span do compilador, capturando-o em um fechamento. Nessa função, vou criar uma matriz, acessar sua extensão e, em seguida, tentar retornar um fechamento que capture essa extensão. Mas nem isso funciona. O compilador reconhece que capturar o Span permite que escape e indica que a vida útil dele depende da matriz local. Esse requisito verificado pelo compilador de que um Span tem escopo restrito significa que retenções e liberações não são necessárias. Obtemos o desempenho de usar um buffer não seguro sem falta de segurança. A família Span inclui versões digitadas e brutas de extensões somente leitura e mutáveis para trabalhar com coleções existentes, bem como um Span de saída que você pode usar para inicializar uma nova coleção. A família também inclui UTF8Span, um novo tipo projetado para processamento Unicode seguro e eficiente.

    De volta ao nosso código, vamos implementar o mesmo método readByte para RawSpan.

    Vamos começar adicionando uma extensão RawSpan...

    e definindo o método readByte.

    A API para RawSpan é um pouco diferente de Data, mas faz a mesma coisa que nossa implementação acima. Ele carrega o primeiro byte, reduz o RawSpan e, em seguida, retorna o valor carregado. Esse método unsafeLoad tem esse nome somente porque pode não ser seguro carregar determinados tipos. Carregar um tipo inteiro integrado, como estamos fazendo aqui, é sempre seguro.

    Agora vamos atualizar nossos métodos de análise.

    Esses dois métodos de análise devem usar RawSpan em vez de Data como parâmetro.

    Também vou precisar fazer uma alteração no local da chamada.

    Em vez de passar os dados em si, vamos obter o RawSpan dos dados e passá-lo para o método de análise. Vou acessar o RawSpan de Data usando a propriedade bytes. O valor rawBytes tem escopo restrito. Eu não poderia retorná-lo a partir desta função, mas posso passá-lo para o método de análise.

    Com essa alteração, consigo atualizar para usar o RawSpan. Para economizar ainda mais trabalho de entrada, também podemos adotar o novo OutputSpan no método de análise.

    Em vez de criar um Data inicializado do zero, vamos usar o novo inicializador rawCapacity, que fornece um OutputSpan para preencher gradualmente os dados não inicializados.

    O OutputSpan controla a quantidade de dados que você grava para poder usar a propriedade count em vez da variável de deslocamento separada.

    Vamos usar outra variação do método write-to que grava no outputSpan em vez de uma instância de Data.

    Vamos dar uma olhada na implementação desse método.

    O método write(to:) pode chamar o método append de OutputSpan para cada canal no pixel. Como OutputSpan é um tipo de escopo restrito projetado para esse tipo de uso, é mais simples e eficiente do que gravar na instância Data e mais seguro do que cair em um ponteiro de buffer não seguro. Terminadas essas alterações, voltarei ao meu teste e vou gravar um novo perfil.

    Vou filtrar em QOI.init.

    No gráfico de chama, vemos que os blocos de swift_retain e swift_release sumiram. Ficou ótimo. Vamos parar por aí e ver os resultados do uso de InlineArray e RawSpan.

    Com essas últimas alterações, nosso trabalho de gerenciamento de memória tornou a análise seis vezes mais rápida, sem recorrer a nenhum código não seguro. Foi 16 vezes mais rápido do que antes, depois de nos livrarmos do algoritmo quadrático, e mais de 700 vezes mais rápido do que como começamos. Abordamos muitos assuntos nesta sessão. Ao revisar a biblioteca de análise de imagens, fizemos duas alterações algorítmicas para operar com mais eficiência e reduzir as alocações. Usamos os novos tipos de biblioteca padrão, InlineArray e RawSpan, para eliminar o gerenciamento de memória em tempo de execução, e aprendemos sobre o novo recurso de linguagem de escopo restrito. A nova biblioteca de Análise binária do Swift foi criada com base nesses mesmos recursos. A biblioteca foi projetada para criar analisadores seguros e eficientes de formatos binários e ajuda os desenvolvedores a lidarem com vários tipos de segurança. A biblioteca fornece um conjunto completo de inicializadores de análise e outras ferramentas que orientam você a consumir com segurança valores de dados binários brutos.

    Vejamos um exemplo de analisador para o cabeçalho QOI, gravado usando a nova biblioteca. Ele mostra vários recursos, incluindo ParserSpan, um tipo de Span bruto personalizado para analisar dados binários. Além de inicializadores de análise, que evitam o fluxo excessivo de inteiros e permitem especificar assinatura, largura de bits e ordem de bytes. A biblioteca também fornece analisadores de validação para seus próprios tipos personalizados representáveis em bruto e operadores de produção opcional para fazer cálculos com segurança com valores não confiáveis e recém-analisados.

    Já estamos usando a biblioteca de Análise binária dentro da Apple, e ela já está disponível publicamente. Incentivamos você a conferi-la e experimentá-la. Participe da comunidade publicando nos fóruns do Swift ou criando problemas ou solicitações pull no GitHub. Agradeço por estarem comigo nesta jornada de otimização do código Swift. Tente usar o Xcode e o Instruments para criar o perfil de um teste das partes críticas de desempenho do seu próprio app. Você pode explorar os novos tipos InlineArray e Span na documentação ou baixando a nova versão do Xcode. Tenha uma ótima WWDC.

    • 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 - Introdução
    • Aprenda a otimizar o desempenho de apps e bibliotecas em Swift usando o Swift 6.2. Os novos tipos 'InlineArray' e 'Span' reduzem alocações, verificações de exclusividade e contagem de referências. Uma nova biblioteca de código aberto para Swift, Binary Parsing, foi lançada para análise binária rápida e segura.

    • 1:19 - App: Formato QOI e parser
    • O app desta sessão da WWDC25 carrega imagens no formato QOI, formato sem perdas com especificação de página única. O analisador de imagens aceita vários métodos de codificação de pixels. O app carrega instantaneamente um ícone pequeno, mas demora alguns segundos para carregar uma foto maior.

    • 2:25 - Algoritmos
    • Quando apps lidam com dados reais, problemas de desempenho podem ocorrer devido ao uso incorreto de algoritmos ou estruturas de dados. Para identificar e resolver esses problemas, use o Instruments, que tem modelos para analisar alocações e liberações, além de identificar código ineficiente com criadores de perfil. O instrumento Time Profiler é útil para identificar problemas de desempenho. Ao analisar as chamadas capturadas e as pilhas de execução, identifique as áreas onde os apps gastam mais tempo. Uma parte do tempo foi gasta em uma chamada de sistema para copiar dados, o 'platform_memmove'. Usando o Instruments, este exemplo analisa um método personalizado chamado 'readByte'. Esse método foi adicionado a uma extensão do tipo 'Data', o que causou cópias excessivas dos dados binários. No exemplo, o método foi substituído pelo mais eficiente 'popFirst()', que reduz os dados do início de uma sequência sem realizar cópias. Essa alteração resolveu o problema de desempenho no método 'readByte'. Após a alteração, o exemplo executou o perfil novamente, e a barra significativa do 'platform_memmove' desapareceu do gráfico de chamas. Os testes de desempenho mostraram aumento na velocidade, e a relação entre o tamanho da imagem e o tempo de análise mudou de quadrática para linear, com um algoritmo mais eficiente.

    • 8:17 - Alocações
    • Foi criado o perfil do app e constatou-se que o analisador de imagens causa alocações e desalocações excessivas de memória envolvendo matrizes. O alto número de alocações, quase um milhão para analisar uma única imagem, indica um problema crítico. A maioria dessas alocações é transitória, de curta duração, podendo ser otimizada. Para identificar a origem dessas alocações desnecessárias, o exemplo utiliza o instrumento Allocations no Instruments. A análise revela que um método chamado 'RGBAPixel.data(channels:)' é o principal responsável. Esse método cria uma matriz toda vez que é chamada, causando um número substancial de alocações. A estrutura do código, que envolve uma cadeia complexa de métodos 'flatMap' e 'prefix', contribui para o problema. Cada etapa dessa cadeia gera novas alocações, pois as matrizes são criadas, niveladas e copiadas repetidamente. Embora a abordagem seja concisa, não é eficiente em termos de memória. Para resolver esse problema, o exemplo reescreve a função de análise. Em vez de depender de alocações intermediárias, ela calcula o tamanho total dos dados resultantes e aloca um único buffer. Essa abordagem elimina a necessidade de alocações repetidas durante o processo de decodificação.

    • 16:30 - Exclusividade
    • O desempenho do app melhorou tanto que os instrumentos de criação de perfil precisaram de mais dados. Após executar o código de análise 50 vezes, os resultados mostraram os 'swift_beginAccess' e 'swift_endAccess', que indicam testes de exclusividade. Esses testes de exclusividade foram causados por propriedades na classe 'State', aninhada dentro do 'QOIParser'. O exemplo move essas propriedades para o tipo pai do analisador para eliminar os testes de exclusividade. Após ajustes no compilador, a verificação de exclusividade foi eliminada, conforme verificado em uma nova execução do perfil.

    • 19:12 - Pilha x heap
    • O exemplo substitui o uso de 'Array' pelo 'InlineArray', uma coleção de tamanho fixo armazenada em linha, que otimiza o uso de memória eliminando a contagem de referências e os testes de exclusividade. É ideal para o cache de pixels, uma matriz de 64 elementos que não muda de tamanho e é modificada no lugar, melhorando o desempenho sem copiar ou compartilhar referências.

    • 21:08 - Contagem de referências
    • No exemplo de otimização do app, é usado o novo tipo 'Span' para melhorar o desempenho e aumentar a segurança da memória. No Instruments, o gráfico de chamas é usado na análise do Time Profiler. A criação de perfis de dados foca o 'QOIParser' e revela que parte do tempo é gasta em operações de contagem de referências no tipo 'Data', devido à sua semântica de copy-on-write. 'Span' e seus tipos relacionados são uma nova forma de trabalhar com memória contígua em uma coleção. Eles usam o recurso não escapável ('~Escapable') do Swift, que vincula o tempo de vida à coleção, garantindo segurança de memória e eliminando a necessidade de gerenciamento manual. Isso permite o acesso eficiente à memória sem os riscos associados a ponteiros não seguros. O exemplo demonstra como usar os tipos 'Span' para reescrever métodos existentes, tornando-os mais simples, seguros e eficientes. Nos métodos de análise de imagem, 'Data' é substituído por 'RawSpan', reduzindo a sobrecarga da contagem de referências. Além disso, o 'OutputSpan' é adotado no processo de análise para otimização adicional, tornando a operação seis vezes mais rápida, sem recorrer a ponteiros não seguros.

    • 29:52 - Biblioteca de análise binária do Swift
    • O Swift Binary Parsing permite criar analisadores seguros e eficientes para formatos binários. Ele oferece ferramentas para lidar com aspectos de segurança, incluindo prevenção de estouro de inteiros, especificação de sinalização, largura de bits e ordem dos bytes. A biblioteca já é usada pela Apple e está disponível para você experimentar e contribuir por meio dos fóruns Swift e do GitHub.

    • 31:03 - Próximas etapas
    • Os principais pontos incluem: Uso do Xcode e Instruments para criar perfis de apps. Análise do desempenho de algoritmos para identificar gargalos. Exploração de soluções para os problemas acima com os novos tipos 'InlineArray' e 'Span' lançados no Swift 6.2.

Developer Footer

  • Vídeos
  • WWDC25
  • Melhore o uso da memória e o desempenho com o Swift
  • Open Menu Close Menu
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • Icon Composer
    • SF Symbols
    Open Menu Close Menu
    • Accessibility
    • Accessories
    • App Store
    • Audio & Video
    • Augmented Reality
    • Business
    • Design
    • Distribution
    • Education
    • Fonts
    • Games
    • Health & Fitness
    • In-App Purchase
    • Localization
    • Maps & Location
    • Machine Learning & AI
    • Open Source
    • Security
    • Safari & Web
    Open Menu Close Menu
    • Documentation
    • Sample Code
    • Tutorials
    • Downloads
    • Forums
    • Videos
    Open Menu Close Menu
    • Support Articles
    • Contact Us
    • Bug Reporting
    • System Status
    Open Menu Close Menu
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles
    • Feedback Assistant
    Open Menu Close Menu
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program
    • News Partner Program
    • Video Partner Program
    • Security Bounty Program
    • Security Research Device Program
    Open Menu Close Menu
    • Meet with Apple
    • Apple Developer Centers
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Academies
    • WWDC
    Get the Apple Developer app.
    Copyright © 2025 Apple Inc. All rights reserved.
    Terms of Use Privacy Policy Agreements and Guidelines