A Microsoft tem feito um grande esforço para melhorar a performance de aplicações .NET e, por isso, tem tratado alocações de objetos em memória com muito carinho. Exemplo disso, é o surgimento da classe ArrayPool.

A classe ArrayPool evita que criemos frequentemente arrays que são usados em um curto período de tempo e, então, descartados. Também evita a criação de arrays grandes que podem gerar segmentação no Large Object Heap.

byte[] buffer = ArrayPool<byte>.Shared.Rent(minLength);
try
{
    // código que usa o buffer;
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}

A idéia básica é que, no lugar de criar uma nova instância de array, nosso código requisite ao ArrayPool usando o método Rent. Então, depois de utilizar o array, devolva ao pool,  utilizando o método Return.

Recomenda-se, quase sempre, a utilização do ArrayPool.Shared, que é compartilhado com toda a aplicação, inclusive com as classes do .NET. Esse Pool é thread safe.

Não devemos utilizar ArrayPool.Shared, apenas em cenários onde nossos arrays tiverem mais de 1.048.576 elementos. Nesses casos, devemos criar um pool customizado.

Importante lembrar que o array fornecido pelo ArrayPool terá, no mímimo, o tamanho que você especificar no parâmetro, podendo ser maior.

Benchmarking

Como sempre, quando estamos fazendo algum trabalho de otimização, é importante “medir, medir, medir” para que tenhamos certeza de que nossas ações estão sendo assertivas.

Comparando alocação padrão com ArrayPool para arrays pequenos (100 bytes)

Antes de sair modificando todos os nossos códigos para usar a ArrayPool vamos examinar se ela vale a pena para arrays pequenos (de até 100 bytes)

using System.Buffers;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

class Program
{
    static void Main() 
        => BenchmarkRunner.Run<Pooling>();
}

[MemoryDiagnoser]
[Config(typeof(DoNotForceGC))] 
public class Pooling
{

    public int ArraySize = 100;

    [Benchmark]
    public void Allocate()
        => DeadCodeEliminationHelper.KeepAliveWithoutBoxing(
            new byte[ArraySize]
            );

    [Benchmark]
    public void RentAndReturn()
    {
        var pool = ArrayPool<byte>.Shared;
        byte[] array = pool.Rent(ArraySize);
        pool.Return(array);
    }
}

public class DoNotForceGC 
    : ManualConfig
{
    public DoNotForceGC()
    {
        Add(Job.Default
            .With(new GcMode
            {
                Force = false 
            }));
    }
}

Usando o BenchmarkDotNet, vimos que alocações manuais (usando new) ainda são muito mais eficientes que a ArrayPool, para arrays pequenos (mesmo com alguma carga na memória).

 

Comparando alocação padrão com ArrayPool para arrays moderados (10 Kbytes)

Vamos agora aplicar o mesmo teste, desta vez, com 10.000 elementos.

Dessa vez, com objetos maiores, percebemos um ganho de mais de 10X usando ArrayPool (que mantem tempo constante)

Comparando alocação padrão com ArrayPool para arrays moderados (100 Kbytes)

Vamos agora aplicar o mesmo teste, desta vez, com 100.000 elementos.

Dessa vez, interessante observar a diferença gritante de performance entre as duas estratégias, sobretudo, considerando a atuação do GC.

Por que ArrayPool se mostrou mais vantajoso para arrays maiores?

Como podemos ver no Benchmarking, ArrayPool  é desaconselhável para cenários em que utilizamos arrays pequenos (100 bytes), mas é extremamente vantajoso em arrays maiores. Por quê? A resposta para essa questão passa pela forma como o .NET gerencia objetos maiores na memória.

Large Object Heap

Em .NET, qualquer objeto que ocupe mais de 85K de memória é considerado grande. Geralmente, esses objetos grandes são arrays (no nosso caso, no terceiro benchmark, usamos um array de 100Kb) ou strings.

Grandes objetos são custosos para mover na memória (isso já pode ser percebido no segundo benchmarking, embora não tenhamos ultrapassado o limite de 85K) e, por isso, não são adequados para o processo de garbage collecting padrão do .NET. Em função disso, recebem tratamento especial, são mantidos em um heap dedicado chamado Large Object Heap (LOH).

Objetos no LOH são gerenciados com uma técnica conhecida como free list. Ou seja, o GC mantem uma lista dos segmentos livres de memória e, quando um novo objeto grande é criado, ocorre uma varredura nessa lista para encontrar uma porção de memória compatível.

Em .NET, objetos grandes não são movidos na memória e podem criar problemas de fragmentação.

Problemas para performance

A fragmentação de memória acaba gerando, em algum momento, desperdício considerável e, em muitos casos, é a justificativa para programas que “comem” toda memória útil do computador (sem justificativa aparente no código).

Além disso, quando o Garbage Collector determina que precisa atuar no Large Object Heap, para atualizar a free list, faz isso disparando uma coleta em Gen2 (a mais pesada e que causa maior tempo de interrupção).

Concluindo

Não utilizamos Arrays diretamente me nossas aplicações com frequência (pelo menos, não diretamente, já que a List mantém um array internamente). Por isso, você talvez tenha dificuldades para imaginar cenários onde precise trabalhar com ArrayPool. Entretanto, se desenvolve código que faça interface com a rede ou leia direto do disco, então poderá se beneficiar desse tipo.

A Microsoft utiliza ArrayPool de forma recorrente dentro do Kestrel e essa foi uma das estratégias responsáveis pelo ganho percebido de performance obtido nas últimas versões.

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
2 Comentários
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Antonio Maniero
Antonio Maniero
2 anos atrás

“Não devemos utilizar ArrayPool.Shared, apenas em cenários onde nossos arrays tiverem mais de” assim fica parecendo meus textos que eu perco o fio da meada 🙂

Não sei se o LOH é tão responsável pela diferença, afinal com 10KB deu uma diferença mais ou menos proporcional. Com 100KB foi um pouco mais dentro da proporção, então ele faz diferença, mas pouca, mas nada absurdo, o que mais faz diferença é o esforço de alocar várias vezes o mesmo objeto. Eu até esperaria que com o 100KB fosse um pouco melhor com os *arrays* direto porque em uma coleta não precisaria transporte. Talvez fosse interessante ver o que acontece se disparar coleta na Gen1 e principalmente na Gen2 pegando o LOH mesmo com volumes menores (forçando criação de mais *arrays*).

Percebemos que alugar um bucket do pool custa caro que dói, em proporção, mas não faz diferença pelo tamanho.

Você fuçou na implementação interna?

Nada crítico, mas achei falta do teste com 1KB já que ele provavelmente seria o ponto de equilíbrio e não mantém tanta distância de um pro outro teste.

Sempre gosto de artigos sobre GC 🙂

Elemar Júnior
Elemar Júnior
2 anos atrás

O erro apontado no texto foi corrigido. 😀 Obrigado.

Quanto a pressão do LOH, considere a quantidade de coletas em Gen2 comparada aos outros cenários. Além disso, considere que, nesse exemplo, não há objetos a serem coletados e não ocorre fragmentação por todos os objetos terem o mesmo tamanho.

1K é exatamente o ponto de equilíbrio.

AUTOR

Elemar Júnior
Fundador e CEO da EximiaCo atua como tech trusted advisor ajudando empresas a gerar mais resultados através da tecnologia.

SOLUÇÕES EXIMIACO

ESTRATÉGIA & EXECUÇÃO EM TI

Simplificamos, potencializamos 
aceleramos resultados usando a tecnologia do jeito certo.

COMO PODEMOS LHE AJUDAR?

Vamos marcar uma conversa para que possamos entender melhor sua situação e juntos avaliar de que forma a tecnologia pode trazer mais resultados para o seu negócio.

COMO PODEMOS LHE AJUDAR?

Vamos marcar uma conversa para que possamos entender melhor sua situação e juntos avaliar de que forma a tecnologia pode trazer mais resultados para o seu negócio.

+55 51 3049-7890 |  [email protected]

2
0
Queremos saber a sua opinião, deixe seu comentáriox
()
x

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.