Fala galera.
Nesses últimos meses tenho trabalhado bastante com otimização de performance em aplicações .NET e gostaria de compartilhar um pouco da minha experiência nessa empreitada.
Problemas de performance geralmente estão relacionados a falta de cuidado ao codificar aliado, em muito casos, à falta de conhecimento.
Nesse post falaremos um pouco sobre o Garbage Colletor.
Apesar de ser um forte aliado, o GC pode acabar sendo um grande vilão da performance da aplicação.
Estudando sobre o GC encontrei essa frase:
“There’s no GC if you don’t allocate. Reducing allocations is key to reducing GC Costs.”
Ajude o Garbage Collector a te ajudar. Aloque menos memória possível!
Lembram das funções malloc e free do C? Pois é… graças ao Garbage Collector não precisamos gerenciar a memória manualmente como era feito no C. Mas a comodidade sempre traz um custo.
Vamos aos conceitos básicos a respeito do Garbage Collector no .NET.
O GC sempre faz 2 varreduras chamadas de Mark and Sweep. Essas ações são fundamentais para recuperação de memória.
- Mark: Identificar objetos vivos
- O objetivo nessa fase é identificar os objetos que estão vivos na memória heap. Esse processo é feito recursivamente. O GC mantém o controle dos objetos que já foram marcados afim de evitar um loop infinito num ciclo de referência entre objetos.
- Sweep: Recuperar objetos mortos
- Nessa fase o GC recupera toda a memória ocupada com objetos que já não estão sendo referenciados.
- Compact: Deixar os objetos vivos juntos
- Depois das duas primeiras fases o GC junta todos os objetos vivos em espaços consecultivos na memória heap. Isso ajuda na performance. É uma espécie de desfragmentador da memória.
Basicamente o trabalho do GC é “só” esse. Parece fácil né? Mas está longe disso!
Na verdade a primeira fase é a mais pesada e requer bastante processamento e inteligência. Isso porque para identificar os objetos que estão vivos o GC leva em consideração os objetos que estão ligados aos roots. Vamos entender o que são roots:
Roots são basicamente variáveis de pilha (stack) / ponteiros que mantém objetos na memória heap. Roots podem também ser variáveis globais. Resumindo: Qualquer ponteiro (que não resida na heap) pra algum objeto na heap pode ser considerado um root.
Ok. Explicado como o GC trabalha internamente vamos entender como o configurá-lo.
O GC pode rodar em dois modos: Concorrente e não concorrente.
- Concorrente: Modo padrão (somente a partir da versão 4.5)
- Esse é o modo padrão. O CLR cria uma thread especial só para o GC assim que o seu aplicativo é iniciado. Essa thread monitora a heap e realiza coletas de lixo ocasionalmente. Ela também agenda a coleta de maneira a não degradar tanto a performance do aplicativo.
- Mas o ponto mais importante é que, na maior parte do tempo o GC roda juntamente com as threads da aplicação, apenas com pequenas interrupções.
- As vezes o GC suspende todas as threads da aplicação mas geralmente essas suspensões são curtas.
- Não-Concorrente
- Não há thread especial para o GC. Todas as threads do aplicativo são suspensas durante a coleta de lixo.
Para configurar é só usar a tag <gcConcurrent>.
<configuration> <runtime> <gcConcurrent enabled="false"/> </runtime> </configuration>
O fato do Garbage Collector ter uma thread só pra ele já é sensacional né? Agora imagine se pudéssemos rodar várias instâncias do GC simultaneamente, uma pra cada processador lógico?
Isso é possível através da tag <GCServer>. Veja abaixo:
<configuration> <runtime> <gcServer enabled="true"/> </runtime> </configuration>
A grande vantagem do modo servidor é que ele tem uma heap separada para cada processador lógico.
Enfim… Devemos considerar habilitar o GCServer, mas sempre fazendo a medição pra ter certeza que o resultado é satisfatório.