Otimização de programas para Garbage Collector

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.

Source: https://habr.com/ru/post/pt453082/


All Articles