High Swap Space Usage — List
Recentemente nos deparamos com a situação de high swap space usage esta situação foi evidenciada pelo alerta configurado pelo DevOps.
Isto significa que, em algum momento a aplicação está descarregando algum objeto que está na stack (não na HEAP) para o disco, liberando memória. Porém este objeto pode ser utilizado em outras partes da aplicação e quando requisitado ele volta para a memória, esta troca é chamada de swap. Este processo com certeza deixa a aplicação mais lenta.
List < T >
As listas são grandes indicativos de que estão causando situações como esta. Existem formas de utilizar as listas genéricas que minimizam o swap. Vou falar um pouco sobre como utilizar uma lista de forma que não estamos habituados.
O primeiro ponto que eu gostaria de destacar está na documentação, a lista genérica possui um parâmetro em seu construtor que é o capacity.
Para analisar o código, sugiro usar o ILSpy ou uma ferramenta de sua preferência para entender o que o método Add()
em uma lista faz. Há também o Reference Sources da Microsoft que pode ajudar e que é web.
Quando o capacity é especificado no construtor, o _items
( T[] - array de T) será criado com o número de elementos passado no capacity. A imagem a seguir exemplifica o capacity como parâmetro do construtor.
Na imagem a seguir, há uma comparação do _size
(.Count da lista) com o _items.Length
.
Quando o Add()
é chamado para adicionar algo em uma lista, o EnsureCapacity(int min)
somente é chamado quando o número de itens adicionados na lista for igual ao número de itens inicializado previamente por conta do capacity conforme imagem a seguir
O método EnsureCapacity(int min)
só é chamado quando o _items
não tenha sido inicializado (não é o caso quando o capacity é especificado) ou quando chegou ao seu limite. A imagem a seguir mostra os detalhes do método.
- Se o capacity não é passado no construtor o
_items
já é inicializado com 4 itens. - Quando o número de itens da lista chega a 4 elementos o número de
_items
passa para 8 e assim por diante.
Neste momento temos um crescimento da lista de forma desnecessária e é neste ponto que os problemas de swap começam.
A grande questão aqui é quando de fato saberemos o tamanho da nossa lista para poder inicializá-la com o capacity esperado.
BenchmarkDotnet — List < T >
Eu utilizei o BenchmarkDotnet como ferramenta de testes de performance para melhorar a visualização da diferença de velocidade, consumo de memória, pressão sobre o GC (Garbage Collector), etc.
Na imagem a seguir eu realizei 2 testes:
FixedGenericListCapacity()
: capacity com valor 10.NoGenericListCapacity()
: sem capacity.
É possível analisar que a alocação de memória (última coluna) da imagem a seguir, é substancialmente menor. Isso ajuda o GC no seu trabalho e evita que este objeto vá parar na Gen 1 ou 2.
Github
Este projeto está disponível no Github.
Para executá-lo basta compilar em release com a linha de comando a seguir
dotnet build -c Release .\BenchmarkDotNet-GenericList.csproj
Depois executar com a linha a seguir
dotnet .\bin\Release\netcoreapp3.1\BenchmarkDotNet-GenericList.dll
BenchmarkDotnet — StringBuilder
Eu aproveitei e fiz o mesmo teste com o StringBuilder, que também possui o capacity sem seu construtor, veja o resultado a seguir.
Conclusão
Sempre que você souber o tamanho da sua lista, use o capacity, nem sempre isso é possível.
Originally published at http://alextochetto.com on March 23, 2021.