Por que grande pilha de objetos e por que nos importamos?

105

Eu li sobre Gerações e heap de objeto grande. Mas ainda não consigo entender qual é o significado (ou benefício) de ter um heap de objeto grande?

O que poderia ter dado errado (em termos de desempenho ou memória) se CLR tivesse contado apenas com a Geração 2 (considerando que o limite para Gen0 e Gen1 é pequeno para lidar com objetos grandes) para armazenar objetos grandes?

Manish Basantani
fonte
6
Isso me dá duas perguntas para os designers do .NET: 1. Por que um defrag LOH não é chamado antes de uma OutOfMemoryException ser lançada? 2. Por que os objetos LOH não têm afinidade por permanecer juntos (grandes preferem o final da pilha e pequenos no início)
Jacob Brewer

Respostas:

195

Uma coleta de lixo não apenas elimina os objetos não referenciados, mas também compacta o heap. Essa é uma otimização muito importante. Isso não apenas torna o uso da memória mais eficiente (sem falhas não utilizadas), mas torna o cache da CPU muito mais eficiente. O cache é realmente um grande negócio nos processadores modernos, eles são uma ordem de magnitude mais rápida do que o barramento de memória.

A compactação é feita simplesmente copiando os bytes. No entanto, isso leva tempo. Quanto maior o objeto, maior a probabilidade de que o custo de copiá-lo supere as possíveis melhorias no uso do cache da CPU.

Então eles executaram um monte de benchmarks para determinar o ponto de equilíbrio. E chegou a 85.000 bytes como o ponto de corte em que a cópia não melhora mais o desempenho. Com uma exceção especial para arrays de double, eles são considerados 'grandes' quando o array tem mais de 1000 elementos. Essa é outra otimização para código de 32 bits, o alocador de heap de objeto grande tem a propriedade especial de alocar memória em endereços alinhados a 8, ao contrário do alocador geracional regular que aloca apenas alinhado a 4. Esse alinhamento é um grande problema para o dobro , ler ou escrever um duplo desalinhado é muito caro. Estranhamente, as informações esparsas da Microsoft nunca mencionam matrizes longas, não tenho certeza do que está acontecendo com isso.

Fwiw, há muita angústia do programador sobre a grande pilha de objetos não ser compactada. Isso invariavelmente é acionado quando eles escrevem programas que consomem mais da metade de todo o espaço de endereço disponível. Seguido pelo uso de uma ferramenta como um criador de perfil de memória para descobrir por que o programa falhou, embora ainda houvesse muita memória virtual não utilizada disponível. Essa ferramenta mostra os buracos no LOH, pedaços de memória não usados ​​onde antes vivia um grande objeto, mas era coletado pelo lixo. Esse é o preço inevitável do LOH, o buraco só pode ser reutilizado por uma alocação para um objeto de tamanho igual ou menor. O verdadeiro problema é assumir que um programa deve consumir toda a memória virtual a qualquer momento.

Um problema que desaparece completamente apenas executando o código em um sistema operacional de 64 bits. Um processo de 64 bits tem 8 terabytes de espaço de endereço de memória virtual disponível, 3 ordens de magnitude a mais do que um processo de 32 bits. Você simplesmente não pode ficar sem buracos.

Resumindo, o LOH torna a execução do código mais eficiente. Ao custo de usar o espaço de endereço de memória virtual disponível menos eficiente.


ATUALIZAÇÃO, .NET 4.5.1 agora oferece suporte à compactação da propriedade LOH, GCSettings.LargeObjectHeapCompactionMode . Cuidado com as consequências, por favor.

Hans Passant
fonte
3
@Hans Passant, poderia esclarecer sobre o sistema x64, quer dizer que esse problema desaparece completamente?
Johnny_D
Alguns detalhes de implementação do LOH fazem sentido, mas alguns me intrigam. Por exemplo, posso entender que se muitos objetos grandes são criados e abandonados, geralmente pode ser desejável excluí-los em massa em uma coleção Gen2 do que aos poucos em coleções Gen0, mas se alguém cria e abandona, por exemplo, uma matriz de 22.000 strings para a qual nenhuma referência externa existe, qual a vantagem de ter as coleções Gen0 e Gen1 marcando todas as 22.000 strings como "vivas", sem levar em conta se existe alguma referência à matriz?
supercat
6
Claro que o problema de fragmentação é o mesmo no x64. A execução do processo do servidor levará apenas mais alguns dias antes de começar.
Lothar
1
Hmm, não, nunca subestime 3 ordens de magnitude. Quanto tempo leva para a coleta de lixo de um heap de 4 terabytes é algo que você não pode evitar descobrir muito antes de chegar perto disso.
Hans Passant
2
@HansPassant Você poderia, por favor, trabalhar nesta declaração: "Quanto tempo leva para coletar o lixo uma pilha de 4 terabytes é algo que você não pode evitar descobrir muito antes de chegar perto disso."
relativamente_random
9

Se o tamanho do objeto for maior do que algum valor fixado (85000 bytes no .NET 1), o CLR o colocará no Large Object Heap. Isso otimiza:

  1. Alocação de objetos (objetos pequenos não são misturados com objetos grandes)
  2. Coleta de lixo (LOH coletado apenas em GC completo)
  3. Desfragmentação de memória (LOH nunca raramente é compactado)
oleksii
fonte
9

A diferença essencial entre Small Object Heap (SOH) e Large Object Heap (LOH) é que a memória em SOH é compactada quando coletada, enquanto LOH não, como este artigo ilustra. Compactar objetos grandes custa muito. Semelhante aos exemplos do artigo, digamos que mover um byte na memória precisa de 2 ciclos e, em seguida, compactar um objeto de 8 MB em um computador de 2 GHz precisa de 8 ms, o que é um alto custo. Considerando que objetos grandes (matrizes na maioria dos casos) são bastante comuns na prática, suponho que essa seja a razão pela qual a Microsoft fixa objetos grandes na memória e propõe o LOH.

BTW, de acordo com este post , LOH geralmente não gera problemas de fragmento de memória.

uva
fonte
1
Carregar grandes quantidades de dados em objetos gerenciados geralmente diminui o custo de 8 ms para compactar o LOH. Na prática, na maioria dos aplicativos de big data, o custo LOH é trivial próximo ao restante do desempenho do aplicativo.
Shiv
3

O principal é que é improvável (e possivelmente um projeto ruim) que um processo crie muitos objetos grandes de curta duração, de modo que o CLR aloca objetos grandes em um heap separado no qual executa o GC em uma programação diferente do heap regular. http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

Myles McDonnell
fonte
Além disso, colocar objetos grandes, digamos, a geração 2 pode acabar prejudicando o desempenho, pois levaria muito tempo para compactar a memória, especialmente se uma pequena quantidade fosse liberada e objetos ENORMES tivessem que ser copiados para um novo local. O LOH atual não é compactado por motivos de desempenho.
Christopher Currens
Acho que é apenas um design ruim porque o GC não lida bem com isso.
CodesInChaos
@CodeInChaos Aparentemente, há algumas melhorias chegando no .NET 4.5
Christian.K
1
@CodeInChaos: Embora possa fazer sentido para o sistema esperar até uma coleção gen2 antes de tentar recuperar a memória mesmo de objetos LOH de curta duração, não consigo ver nenhuma vantagem de desempenho em declarar objetos LOH (e quaisquer objetos aos quais eles contêm referências) ao vivo incondicionalmente durante as coletas gen0 e gen1. Existem algumas otimizações que são possíveis por tal suposição?
supercat
@supercat Eu olhei o link mencionado por Myles McDonnell. Meu entendimento é: 1. A coleta LOH acontece em um GC gen 2. 2. A coleção LOH não inclui compactação (no momento em que o artigo foi escrito). Em vez disso, ele marcará objetos mortos como reutilizáveis ​​e esses buracos servirão a futuras alocações de LOH se forem grandes o suficiente. Por causa do ponto 1, considerando que um GC de geração 2 seria lento se houvesse muitos objetos na geração 2, acho melhor evitar o uso de LOH tanto quanto possível neste caso.
Robbie Fan
0

Não sou um especialista em CLR, mas imagino que ter um heap dedicado para objetos grandes pode evitar varreduras de GC desnecessárias dos heaps geracionais existentes. Alocar um objeto grande requer uma quantidade significativa de memória livre contígua . Para fornecer isso a partir dos "buracos" espalhados nas pilhas de gerações, você precisaria de compactações frequentes (que são feitas apenas com ciclos de GC).

Chris Shain
fonte