O que é pool de objetos? Melhorando o desempenho da memória em C#
Alocar memória em C# é relativamente caro e é um ponto-chave de otimização para qualquer aplicativo de desempenho crítico. O pool de objetos é uma técnica que pode ajudar a reduzir a sobrecarga de um aplicativo com uso intensivo de memória.
Como o pool de objetos melhora o desempenho?
No final das contas, o desempenho dos computadores é basicamente limitado a duas coisas: velocidade de processamento da CPU e desempenho da memória. O primeiro pode ser melhorado com algoritmos mais eficientes que usam menos ciclos de clock, mas geralmente é mais difícil otimizar o uso de memória, especialmente ao trabalhar com grandes conjuntos de dados e escalas de tempo muito curtas.
O pool de objetos é uma técnica usada para reduzir alocações de memória. Muitas vezes não há como contornar a necessidade de alocar memória, mas talvez você não precise alocar com tanta frequência. As alocações de objetos são lentas por alguns motivos: a memória subjacente é alocada na pilha (que é muito mais lenta do que as alocações de pilha de tipo de valor) e objetos complexos podem ter construtores com alto desempenho. Além disso, como a memória é baseada em heap, o coletor de lixo precisará limpá-la, o que pode prejudicar o desempenho se você acioná-la com muita frequência.
Por exemplo, digamos que você tenha um loop que é executado várias vezes e aloca um novo objeto como uma lista para cada execução. Isso é muita memória sendo usada e não está sendo limpa até que tudo termine e a coleta de lixo seja executada. O código a seguir será executado 10.000 vezes e deixará 10.000 listas sem proprietário alocadas na memória no final da função.
Quando o coletor de lixo finalmente for executado, será muito difícil limpar todo esse lixo, o que afetará negativamente o desempenho enquanto espera a conclusão do GC.
Em vez disso, uma abordagem mais sensata é inicializá-lo uma vez e reutilizar o objeto. Isso garante que você reutilize o mesmo espaço na memória, em vez de esquecê-lo e deixar que o coletor de lixo cuide dele. Não é mágica e você terá que limpar eventualmente.
Para este exemplo, a abordagem reciclável seria executar new List
antes do loop para fazer a primeira alocação e, em seguida, executar .Clear
ou redefinir os dados para economizar espaço na memória e lixo criado. Depois que esse loop terminar, restará apenas uma lista na memória, que é bem menor que 10.000 delas.
Object Pooling é basicamente uma implementação genérica deste conceito. É uma coleção de objetos que podem ser reutilizados. Não há interface oficial para um, mas em geral eles têm um armazenamento de dados interno e implementam dois métodos: GetObject()
e ReleaseObject()
.
Em vez de alocar um novo objeto, você solicita um do pool de objetos. O pool pode criar um novo objeto se não tiver um disponível. Então, quando terminar, você libera esse objeto de volta para a piscina. Em vez de jogar o objeto no lixo, o pool de objetos o mantém alocado, mas o limpa de todos os dados. Na próxima vez que você executar GetObject
, ele retornará o novo objeto vazio. Para objetos grandes, como Listas, fazer isso é muito mais fácil na memória subjacente.
Você precisa se certificar de que está liberando o objeto antes de usá-lo novamente, porque a maioria das piscinas terá um número máximo de objetos vazios que eles mantêm à mão. Se você tentar obter 1.000 objetos da piscina, você a secará e começará a alocá-los normalmente mediante solicitação, o que anula o propósito de uma piscina.
Em casos de desempenho crítico, especialmente ao trabalhar frequentemente com muitos dados repetidos, o pool de objetos pode melhorar drasticamente o desempenho. No entanto, não é uma pegadinha e muitas vezes você não gostaria de agrupar objetos. As alocações com new
ainda são muito rápidas, então, a menos que você esteja alocando grandes blocos de memória com muita frequência ou alocando objetos com construtores de alto desempenho, geralmente é melhor usar apenas new
em vez de agrupar desnecessariamente, especialmente considerando que o próprio pool adiciona sobrecarga extra e complexidade à alocação. Alocar um único objeto regular sempre será mais rápido do que alocar um único objeto agrupado; a diferença de desempenho ocorre quando você está alocando muitas vezes.
Você também precisará garantir que sua implementação de pooling limpe adequadamente o estado do objeto. Caso contrário, você pode correr o risco de GetObject
retornar algo que tenha dados obsoletos. Isso pode ser um grande problema se, por exemplo, você retornar os dados de um usuário diferente ao buscar outra pessoa. Geralmente, não é um problema se feito corretamente, mas é algo para se ter em mente.
Se você quiser usar Object Pools, a Microsoft oferece uma implementação em Microsoft.Extensions.ObjectPool
. Se você quiser implementá-lo sozinho, pode abrir o código-fonte para verificar como funciona em DefaultObjectPool
. Geralmente, você terá um pool de objetos diferente para cada tipo, com um número máximo de objetos para manter no pool.