Há pouco tempo, apareceu um excelente artigo sobre Habré Optimization of garbage collection em um serviço .NET altamente carregado . Este artigo é muito interessante, pois os autores, armados com a teoria, fizeram o impossível anteriormente: eles otimizaram sua aplicação usando o conhecimento do GC. E se antes não tínhamos idéia de como esse GC funciona, agora ele é apresentado a nós em uma bandeja de prata através dos esforços de Konrad Cocos em seu livro Pro .NET Memory Management . Que conclusões tirei para mim? Vamos fazer uma lista de áreas problemáticas e pensar em como resolvê-las.
No recente workshop do CLRium # 5: Garbage Collector, conversamos sobre a GC o dia todo. No entanto, decidi publicar um relatório com decodificação de texto. Esta é uma palestra sobre conclusões sobre a otimização de aplicativos.
Reduza a conectividade entre gerações
O problema
Para otimizar a velocidade da coleta de lixo, o GC coleta a geração mais jovem sempre que possível. Mas, para fazer isso, ele também precisa de informações sobre os links das gerações mais antigas (neste caso, elas agem como uma raiz extra): da tabela de cartões.
Ao mesmo tempo, um link da geração mais antiga para a mais nova força você a cobrir a área com uma mesa de cartas:
- 4 bytes se sobrepõem 4 kb ou no máx. 320 objetos - para arquitetura x86
- 8 bytes se sobrepõem a 8 kb ou máx. 320 objetos - para arquitetura x64
I.e. O GC, verificando a tabela de cartões, encontrando um valor diferente de zero, é forçado a verificar no máximo 320 objetos quanto à presença de links de saída em nossa geração.
Portanto, links esparsos na geração mais jovem tornarão o GC mais demorado
Solução
- Localize objetos com conexões na geração mais jovem - nas proximidades;
- Se o tráfego de objetos de geração zero é suposto, use pull. I.e. faça um conjunto de objetos (não haverá novos: não haverá objetos de geração zero). Além disso, "aquecendo" o pool com dois GCs consecutivos para garantir que seu conteúdo falhe na segunda geração, você evita links para a geração mais jovem e tem zeros na mesa de cartas;
- Evite links para a geração mais jovem;
Evite conectividade forte
O problema
A seguir, a partir dos algoritmos da fase de compactação de objetos no SOH:
- Para compactar a pilha, você precisa percorrer a árvore e verificar todos os links, corrigindo-os para novos valores
- Além disso, os links da tabela de cartões afetam grupos inteiros de objetos
Portanto, a forte conectividade geral dos objetos pode levar ao subsidência durante o GC.
Solução
- Tenha objetos fortemente conectados por perto, em uma geração
- Evite links desnecessários em geral (por exemplo, em vez de duplicar os links this-> handle, use o já existente this-> Service-> handle)
- Evite código com conectividade oculta. Por exemplo, fechamentos
Monitorar o uso do segmento
O problema
Durante o trabalho intensivo, uma situação pode surgir quando a alocação de novos objetos leva a atrasos: a alocação de novos segmentos sob a pilha e sua decomposição adicional ao limpar o lixo
Solução
- Usando Utilitários PerfMon / Sysinternal para controlar os pontos de seleção de novos segmentos e sua desativação e liberação
- Se estamos falando de LOH, que é um tráfego de buffer denso, use ArrayPool
- Se estivermos falando sobre SOH, verifique se os objetos da mesma vida útil estão realçados nas proximidades, fornecendo Sweep em vez de Collect
- SOH: use conjuntos de objetos
Não aloque memória nas seções carregadas do código
O problema
A seção carregada do código aloca memória:
- Como resultado, o GC seleciona uma janela de alocação não de 1 KB, mas de 8 KB.
- Se a janela ficar sem espaço, isso levará a um GC e a expansão da zona fechada
- Um fluxo denso de novos objetos fará com que objetos de curta duração de outros encadeamentos sejam rapidamente transferidos para a geração mais antiga com piores condições de coleta de lixo
- O que aumentará o tempo de coleta de lixo
- O que levará a um Stop the World mais longo, mesmo no modo simultâneo
Solução
- Proibição completa do uso de fechamentos em seções críticas do código
- Proibição completa do boxe em seções críticas do código (você pode usar emulação puxando, se necessário)
- Onde for necessário criar um objeto temporário para armazenamento de dados, use estruturas. Melhor é ref struct. Quando o número de campos for maior que 2, transmita por ref
Evite alocações de memória desnecessárias no LOH
O problema
A colocação de matrizes no LOH leva à fragmentação ou ponderação do procedimento de GC
Solução
- Use a divisão de matrizes em sub-matrizes e uma classe que encapsule a lógica de trabalhar com tais matrizes (ou seja, em vez de Lista <T>, onde o mega-array é armazenado, seu MyList com array [] [], dividindo o array um pouco mais curto))
- Matrizes irão para SOH
- Depois de algumas coletas de lixo, elas se deitam ao lado de objetos sempre vivos e deixam de influenciar a coleta de lixo
- Controle o uso de matrizes duplas com um comprimento de mais de 1000 elementos.
Onde justificado e possível, use a pilha de threads
O problema
Há vários objetos ultracurtos ou objetos que vivem em uma chamada de método (incluindo chamadas internas). Eles criam tráfego de objetos
Solução
- Usando alocação de memória na pilha sempre que possível:
- Não carrega um monte
- Não carrega GC
- Liberando memória - Instantâneo
- Use
Span T x = stackalloc T[];
em vez de new T[]
que possível - Use
Span/Memory
que possível - Converter algoritmos em tipos de
ref stack
(StackList: struct, ValueStringBuilder )
Objetos grátis o mais cedo possível
O problema
Concebidos como de curta duração, os objetos caem no gen1 e, às vezes, no gen2.
Isso resulta em um GC mais pesado que dura mais tempo
Solução
- Você deve liberar a referência do objeto o mais cedo possível
- Se um algoritmo longo contém código que funciona com qualquer objeto, ele é espaçado pelo código. Mas que podem ser agrupados em um só lugar, é necessário agrupá-lo, permitindo que sejam coletados mais cedo.
- Por exemplo, na linha 10, a coleção foi retirada e na linha 120, foi filtrada.
Não há necessidade de chamar GC.Collect ()
O problema
Muitas vezes parece que se você ligar para GC.Collect (), isso resolverá a situação
Solução
- É muito mais correto aprender os algoritmos de operação do GC, examinar o aplicativo em ETW e outras ferramentas de diagnóstico (JetBrains dotMemory, ...)
- Otimize as áreas mais problemáticas
Evite fixar
O problema
A fixação coloca vários problemas:
- Complica a coleta de lixo
- Cria espaços de memória livres (itens de lista livre de nós, tabela de tijolos, buckets)
- Pode deixar alguns objetos na geração mais jovem, enquanto cria links da mesa de cartas
Solução
Se não houver outra saída, use Fixed () {}. Esse método de confirmação não faz uma confirmação real: só acontece quando o GC trabalha dentro de chaves.
Evitar finalização
O problema
A finalização não é chamada deterministicamente:
- Dispose () não convidado resulta em finalização com todos os links de saída do objeto
- Objetos dependentes atrasam mais que o planejado
- Envelhecimento, mudança para gerações mais velhas
- Se, ao mesmo tempo, eles contêm links para os mais jovens, eles geram links da tabela de cartões
- Complicando a montagem das gerações mais antigas, fragmentando-as e levando à compactação em vez de varrer
Solução
Ligue suavemente para Dispose ()
Evite muitos threads
O problema
Com um grande número de threads, o contexto de alocação cresce, conforme eles são alocados para cada thread:
- Como resultado, o GC.Collect vem mais rápido.
- Devido à falta de espaço no segmento efêmero, o Collect seguirá a Varredura Coletiva
Solução
- Controlar o número de threads pelo número de núcleos
Evite o tráfego de objetos de tamanhos diferentes
O problema
Ao trafegar objetos de tamanhos e vidas diferentes, ocorre fragmentação:
- Aumentar taxa de fragmentação
- Acionamento de coleção com uma fase de mudança de endereço em todos os objetos de referência
Solução
Se o tráfego de objetos é suposto:
- Verifique a presença de campos extras, aproximando o tamanho
- Verifique a falta de manipulação de string: sempre que possível, substitua por ReadOnlySpan / ReadOnlyMemory
- Libere o link o mais rápido possível
- Tire vantagem de puxar
- Aqueça caches e pools com um GC duplo para compactar objetos. Assim, você evita problemas com a mesa de cartas.