Desde o início, quando comecei a programar, surgiu a questão sobre o que usar para melhorar o desempenho: estrutura ou classe; quais matrizes são melhores para usar e como. Em relação às estruturas, a Apple agradece seu uso, explicando que elas são melhores em otimização, e toda a essência da linguagem Swift é a estrutura. Mas há quem não concorde com isso, porque você pode simplificar lindamente o código herdando uma classe da outra e trabalhando com essa classe. Para acelerar o trabalho com as classes, criamos diferentes modificadores e objetos que foram otimizados especificamente para as classes, e já é difícil dizer o que será mais rápido e, nesse caso.
Para organizar todos os pontos no "e", escrevi vários testes que usam as abordagens usuais do processamento de dados: passando para um método, copiando, trabalhando com matrizes e assim por diante. Decidi não tirar grandes conclusões, todos decidirão por si mesmos se vale a pena acreditar nos testes, poderão fazer o download do projeto e ver como ele funcionará para você, e tentar otimizar a operação de um teste específico. Talvez até novos chips sejam lançados que eu não mencionei, ou eles são tão raramente usados que eu simplesmente não ouvi falar deles.
PS: Comecei a trabalhar em um artigo no Xcode 10.3 e pensei em tentar comparar sua velocidade com o Xcode 11, mas ainda assim, o artigo não trata de comparar dois aplicativos, mas da velocidade de nossos aplicativos. Não tenho dúvidas de que o tempo de execução das funções diminuirá e o que foi mal otimizado se tornará mais rápido. Como resultado, esperei pelo novo Swift 5.1 e decidi testar as hipóteses na prática. Boa leitura.
Teste 1: comparar matrizes em estruturas e classes
Suponha que tenhamos uma classe e desejemos colocar os objetos dessa classe em uma matriz, a ação usual em uma matriz é fazer um loop através dela.
Em uma matriz, ao usar classes e tentar percorrê-lo, o número de links aumenta; após a conclusão, o número de links para o objeto diminui.
Se percorrermos a estrutura, no momento em que o objeto for chamado pelo índice, uma cópia do objeto será criada, olhando para a mesma área de memória, mas marcada como imutável. É difícil dizer o que é mais rápido: aumentar o número de links para um objeto ou criar um link para uma área na memória com a falta da capacidade de alterá-lo. Vamos verificar na prática:
Fig. 1: Comparação de obter uma variável de matrizes com base em estruturas e classesTeste 2. Comparação ContiguousArray vs Array
O mais interessante é comparar o desempenho de uma matriz (Matriz) com uma matriz de referência (ContiguousArray), necessária especificamente para trabalhar com as classes armazenadas na matriz.
Vamos verificar o desempenho para os seguintes casos:
ContiguousArray armazenando uma estrutura com o tipo de valor
ContiguousArray armazenando struct com String
ContiguousArray armazenando classe com tipo de valor
ContiguousArray armazenando classe com String
Matriz armazenando struct com tipo de valor
Matriz armazenando struct com String
Matriz armazenando classe com tipo de valor
Matriz armazenando classe com String
Como os resultados do teste (testes: passando para uma função com otimização embutida desativada, passando para uma função com otimização embutida ativada, excluindo elementos, adicionando elementos, acesso seqüencial a um elemento em um loop) incluirão um grande número de testes (para 8 matrizes de 5 testes cada) , Darei os resultados mais significativos:
- Se você chamar uma função e passar uma matriz para ela, desativando a linha, essa chamada será muito cara (para classes baseadas na String de referência, é 20.000 vezes mais lenta, para classes baseadas em Value, o tipo é 60.000 vezes, pior com o otimizador embutido desativado) .
- Se a otimização (em linha) funcionar para você, a degradação deve ser esperada apenas 2 vezes, dependendo do tipo de dados adicionado a qual matriz. A única exceção foi o tipo de valor, envolto em uma estrutura localizada no ContiguousArray - sem degradação do tempo.
- Remoção - a diferença entre a matriz de referência e a usual era de cerca de 20% (a favor da matriz usual).
- Anexar - ao usar objetos agrupados em classes, o ContiguousArray tinha uma velocidade cerca de 20% mais rápida que o Array com os mesmos objetos, enquanto o Array era mais rápido ao trabalhar com estruturas do que o ContiguousArray com estruturas.
- O acesso aos elementos da matriz ao usar wrappers de estruturas acabou sendo mais rápido do que qualquer wrapper nas classes, incluindo o ContiguousArray (cerca de 500 vezes mais rápido).
Na maioria dos casos, o uso de matrizes regulares para trabalhar com objetos é mais eficiente. Usado antes, usamos mais.
A otimização do loop para matrizes é atendida pelo inicializador de coleção lenta, que permite percorrer toda a matriz apenas uma vez, mesmo se você usar vários filtros ou mapas sobre os elementos da matriz.
Ao usar estruturas como uma ferramenta de otimização, existem armadilhas, como o uso de tipos que são internamente referenciados na natureza: seqüências de caracteres, dicionários, matrizes de referência. Então, quando uma variável que armazena um tipo de referência em si é inserida em uma função, uma referência adicional é criada para cada elemento que é uma classe. Isso tem outro lado, sobre isso um pouco mais. Você pode tentar usar uma classe de wrapper sobre uma variável. Em seguida, o número de links ao passar para a função aumentará apenas para ela, e o número de links para valores dentro da estrutura permanecerá o mesmo. Em geral, quero ver quantas variáveis de um tipo de referência devem estar na estrutura para que seu desempenho diminua mais que o desempenho de classes com os mesmos parâmetros. Existe um artigo na web chamado “Pare de usar estruturas!”, Que faz a mesma pergunta e a responde. Baixei o projeto e decidi descobrir o que acontece onde e em que casos temos estruturas lentas. O autor mostra o baixo desempenho das estruturas em comparação com as classes, argumentando que criar um novo objeto é muito mais lento do que aumentar a referência ao objeto é um absurdo (então removi a linha em que um novo objeto é criado no loop todas as vezes). Mas se não criarmos um link para o objeto, mas simplesmente passá-lo para uma função para trabalhar com ele, a diferença de desempenho será muito insignificante. Cada vez que colocamos em
linha (nunca) uma função, nosso aplicativo deve executá-la e não criar código em uma string. A julgar pelos testes, a Apple fez com que o objeto passado para a função fosse ligeiramente modificado. Para estruturas, o compilador altera a mutabilidade e torna o acesso a propriedades não mutáveis do objeto preguiçosamente. Algo semelhante acontece na classe, mas ao mesmo tempo aumenta o número de referências ao objeto. E agora temos um objeto lento, todos os seus campos também são preguiçosos e toda vez que chamamos uma variável de objeto, ela é inicializada. Nisso, as estruturas não têm igual: quando uma função chama duas variáveis, a estrutura do objeto é apenas ligeiramente inferior à classe em velocidade; quando você liga para três ou mais, a estrutura sempre será mais rápida.
Teste 3: Compare o desempenho de Estruturas e Classes armazenando grandes classes
Além disso, mudei levemente o próprio método, que foi chamado quando outra variável foi adicionada (dessa forma, três variáveis foram inicializadas no método, e não duas, como no artigo), e que não haveria um estouro de Int, substituí as operações nas variáveis pela soma e subtração. Adicionadas métricas de tempo mais claras (na captura de tela são segundos, mas não é tão importante para nós, é importante entender as proporções resultantes), removendo a estrutura de Darwin (não uso em projetos, talvez em vão, não há diferenças nos testes antes / depois de adicionar a estrutura em meu teste), a inclusão da otimização máxima e do build no release (parece que isso será mais honesto), e aqui está o resultado:
Fig. 2: Desempenho de estruturas e classes do artigo "Pare de usar estruturas"As diferenças nos resultados dos testes são insignificantes.
Teste 4: Função que aceita genérico, protocolo e função sem genérico
Se pegarmos uma função genérica e passarmos dois valores para lá, unidos apenas pela capacidade de comparar esses valores (func min), o código de três linhas se transformará no código de oito (como a Apple diz). Mas nem sempre é esse o caso, o Xcode possui métodos de otimização nos quais, se, ao chamar uma função, vê que dois valores estruturais são passados para ela, gera automaticamente uma função que utiliza duas estruturas e não copia mais os valores.
Fig. 3: Função Genérica TípicaEu decidi testar duas funções: na primeira, o tipo de dados genérico é declarado, a segunda aceita apenas protocolo. Na nova versão do Protocolo Swift 5.1, é um pouco mais rápido que o Genérico (antes do Swift 5.1 os protocolos eram 2 vezes mais lentos), embora de acordo com a Apple deva ser o contrário, mas quando se trata de passar por uma matriz, já precisamos digitar, o que diminui a velocidade Genéricos (mas ainda são ótimos, porque são mais rápidos que os protocolos):
Fig. 4: Comparação de funções de host genérico e de protocolo.Teste 5: compare a chamada do método pai e a nativa e, ao mesmo tempo, verifique a classe final para essa chamada
O que sempre me interessou é o quão lentamente as aulas trabalham com um grande número de pais, a rapidez com que uma classe chama suas funções e as de um pai. Nos casos em que estamos tentando chamar um método que leva uma classe, o despacho dinâmico entra em jogo. O que é isso Toda vez que um método ou variável é chamado dentro de nossa função, uma mensagem é gerada solicitando ao objeto essa variável ou método. O objeto, recebendo essa solicitação, começa a procurar o método na tabela de despacho de sua classe e, se uma substituição do método ou variável foi chamada, pega e retorna, ou alcança recursivamente a classe base.
Fig. 5: Chamadas de método de classe, para teste de despachoVárias conclusões podem ser tiradas do teste acima: quanto maior a classe de classes pai, mais lenta ela funcionará e que a diferença de velocidade é tão pequena que pode ser negligenciada com segurança, a otimização de código mais provável fará com que não haja diferença de velocidade. Neste exemplo, o modificador de classe final não tem uma vantagem, pelo contrário, o trabalho da classe é ainda mais lento, possivelmente devido ao fato de não se tornar uma função realmente rápida.
Teste 6: Chamando uma variável com modificador final em relação a uma variável de classe regular
Também com resultados muito interessantes ao atribuir o modificador final a uma variável, você pode usá-lo quando tiver certeza de que a variável não será reescrita em nenhum lugar nos herdeiros da classe. Vamos tentar colocar o modificador final em uma variável. Se em nosso teste criamos apenas uma variável e chamamos uma propriedade nela, ela seria inicializada uma vez (o resultado é a seguir). Se honestamente criarmos cada vez que um novo objeto e solicitar sua variável, a velocidade diminuirá visivelmente (o resultado está acima):
Fig. 6: Chamar variável finalObviamente, o modificador não foi para o benefício da variável e é sempre mais lento que seu concorrente.
Teste 7: Problema de polimorfismo e protocolos para estruturas. Ou o desempenho de um contêiner existente
Problema: se usarmos um protocolo que suporta um determinado método e várias estruturas herdadas desse protocolo, o que o nosso compilador pensará quando colocarmos estruturas com diferentes volumes de valores armazenados em uma matriz, unidos pelo protocolo original?
Para resolver o problema de chamar um método predefinido nos herdeiros, é usado o mecanismo da Tabela de Testemunhas de Protocolo. Ele cria estruturas de shell que fazem referência aos métodos necessários.
Para resolver o problema do armazenamento de dados, é usado um contêiner existencial. Ele armazena em si 5 células de informação, cada uma com 8 bytes. Nos três primeiros, o espaço é alocado para os dados armazenados na estrutura (se eles não couberem, cria um link para o heap no qual os dados são armazenados), o quarto armazena informações sobre os tipos de dados que são usados na estrutura e nos informa como gerenciar esses dados. , o quinto contém referências aos métodos do objeto.
Figura 7. Comparação do desempenho de uma matriz que cria um link para um objeto e que o contémEntre o primeiro e o segundo resultado, o número de variáveis triplicou. Em teoria, eles devem ser colocados em um contêiner, são armazenados nesse contêiner e a diferença de velocidade é devida ao volume da estrutura. Curiosamente, se você reduzir o número de variáveis na segunda estrutura, o tempo de operação não será alterado, ou seja, o contêiner armazenará 3 ou 2 variáveis, mas, aparentemente, existem condições especiais para uma variável que aumentam significativamente a velocidade. A segunda estrutura se encaixa perfeitamente no contêiner e difere do volume da terceira pela metade, o que gera uma forte degradação no tempo de execução, em comparação com outras estruturas.
Um pouco de teoria para otimizar seus projetos
Os seguintes fatores podem influenciar o desempenho das estruturas:
- onde suas variáveis são armazenadas (heap / stack);
- a necessidade de contagem de referência para propriedades;
- métodos de agendamento (estático / dinâmico);
- O Copy-On-Write é usado apenas por estruturas de dados que são tipos de referência que pretendem ser estruturas (String, Matriz, Conjunto, Dicionário) sob o capô.
Vale esclarecer imediatamente que o mais rápido de todos serão os objetos que armazenam propriedades na pilha, não usam contagem de referência com o método estático de exame médico.
Do que as classes são ruins e perigosas em comparação com estruturas
Nem sempre controlamos a cópia de nossos objetos e, se fizermos isso, podemos obter muitas cópias que serão difíceis de gerenciar (criamos objetos no projeto que são responsáveis por formar a exibição, por exemplo).
Eles não são tão rápidos quanto estruturas.
Se tivermos um link para um objeto e estivermos tentando gerenciar nosso aplicativo em um estilo multithread, podemos obter a Condição de Corrida quando nosso objeto for usado em dois lugares diferentes (o que não é tão difícil, porque um projeto criado com o Xcode é sempre um pouco mais lento, que a versão Store).
Se tentarmos evitar a Condição de Corrida, gastamos muitos recursos no Lock e em nossos dados, que começam a consumir recursos e desperdiçar tempo em vez de processamento rápido e obtemos objetos ainda mais lentos que os mesmos construídos em estruturas.
Se fizermos todas as ações acima em nossos objetos (links), a probabilidade de conflitos imprevistos é alta.
A complexidade do código está aumentando por causa disso.
Mais código = mais bugs, sempre!
Conclusões
Eu pensei que as conclusões deste artigo são simplesmente necessárias, porque eu não quero ler o artigo de tempos em tempos, e uma lista consolidada de pontos é simplesmente necessária. Resumindo as linhas sob os testes, quero destacar o seguinte:
- As matrizes são melhor colocadas em uma matriz.
- Se você deseja criar uma matriz a partir de classes, é melhor escolher uma matriz regular, pois o ContiguousArray raramente oferece vantagens e elas não são muito altas.
- A otimização em linha acelera o trabalho, não o desative.
- O acesso aos elementos Array é sempre mais rápido que o acesso aos elementos ContiguousArray.
- As estruturas são sempre mais rápidas que as classes (a menos que você ative a otimização do módulo inteiro ou otimização semelhante).
- Ao passar um objeto para uma função e chamar suas propriedades, a partir do terceiro, a estrutura é mais rápida que as classes.
- Quando você passa um valor para uma função escrita para Genérico e Protocolo, o Genérico é mais rápido.
- Com herança de várias classes, a velocidade da chamada de função diminui.
- As variáveis marcaram o trabalho final mais lentamente do que os pimentões comuns.
- Se uma função aceitar um objeto que combine vários objetos com o protocolo, ela funcionará rapidamente se apenas uma propriedade estiver armazenada nela e se degradará bastante ao adicionar mais propriedades.
Referências:
medium.com/@vhart/protocols-generics-and-existential-containers-wait-what-e2e698262ab1developer.apple.com/videos/play/wwdc2016/416developer.apple.com/videos/play/wwdc2015/409developer.apple.com/videos/play/wwdc2016/419medium.com/commencis/stop-using-structs-e1be9a86376fCódigo fonte do teste