Pensando em performance — Parte 1

Cristiano Rodrigues
4 min readMar 2, 2021

Tem um tempinho que não escrevo nada, então resolvi começar uma pequena série sobre performance em códigos .NET.

E para estrear a série vou falar sobre o inofensivo List.

Analisando os códigos

Começamos com o código abaixo.

O código é bem simples e quase que comum em algum trecho de alguma aplicação do mundo real. Então vamos analisá-lo por outro ângulo, usando o WinDbg. Vamos tirar um Dump da memória do processo e analisar com um pouco mais de profundidade (em breve um artigo falando da coleta do dump).

Após o dump gerado, vamos carregá-lo no WinDbg e verificar como está a stack.

Até aqui tudo normal. Estamos com uma referência da nossa lista usando a nossa estrutura.

Vamos analisar um pouco mais de perto.

Está tudo certo! Os itens estão na lista e só temos uma única referência da nossa lista.

Então vamos olhar o Heap.

Opa, de onde saíram esses 10 arrays de MinhaEstrutura? Não criamos nada!

Isso é verdade, não criamos nada, mas deixamos a lista com tamanho dinâmico, ou seja, ao incluir um item na lista será criado um array de 4 elementos e ao completar 4 elementos um novo array com 8 elementos será criado e o array de 4 elementos será "descartado". Ao completar os 8 elementos um novo array com 16 elementos será criado e o array de 8 elementos será "descartado". A cada criação de um novo array os dados são "copiados" do antigo para o novo e novos elementos são incluídos no array. Este processo será feito até que todos os elementos caibam dentro de um único array.

Comprovando a existência dos arrays.

Sendo o último array com 1024 elementos.

Esses arrays serão descartados pelo GC no futuro e isso pode se tornar um problema, aumentando a pressão por memória e obrigando o GC a ser executado com mais frequência e GC sendo executado com mais frequência significa que a aplicação ficará "congelada" até o GC finalizar o seu trabalho (explicação bem resumida e simplificada).

Então como podemos evitar isso? Não precisa de muito, basta informar a quantidade de elementos que serão adicionados na sua lista no momento da criação, através do construtor da lista. Lógico nem sempre sabemos o tamanho! Então é por isso que é bom entender os dados/negócio para conseguir uma volumetria aproximada e conseguir inicializar a lista para evitar que o processo de criação comece de 4 elementos. Você não precisa ser certeiro, mas se conseguir se aproximar ou até mesmo diminuir a quantidade de criação de arrays já será um ótimo benefício.

No nosso caso informaremos através do construtor da classe uma capacidade de 1000 elementos. Neste momento será alocado um array de 1000 elementos suficiente para o nosso exemplo.

Coletando um novo dump e analisando o Heap.

Vejam que agora temos apenas uma instância de um array da MinhaEstrutura. Então vamos ver com mais detalhes o objeto.

Com base no resultado podemos confirmar que temos um único array de 1000 elementos, ou seja, não "largamos" objetos para serem descartados pelo GC no futuro.

Comparando a eficiência dos códigos

Com base no resultado o método que inicializa a lista com capacidade de 1000 elementos roda mais rápido e aloca menos memória.

Conclusão

São pequenas coisas que fazem a diferença no final. Por isso é importante entender como elas funcionam. No nosso caso, uma pequena alteração na capacidade de elementos durante a construção de uma simples lista demonstrou um ganho considerável, imagine levar isso para os códigos da vida real! Qual seria o seu ganho?

Não esquece de medir, medir e medir novamente para comparar o código anterior com o novo.

É com dados que comprovamos o ganho de performance!

Até a próxima!

--

--

Cristiano Rodrigues

Microsoft MVP | Solutions Architect with more than 15 years in software development. In love for Docker and Kubernetes, a specialist in .NET and SQL Server.