Quando é uma boa ideia forçar a coleta de lixo?


135

Então, eu estava lendo uma pergunta sobre forçar o coletor de lixo C # a executar, onde quase todas as respostas são iguais: você pode fazê-lo, mas não deveria - exceto em casos muito raros . Infelizmente, ninguém lá elabora quais são esses casos.

Você pode me dizer em que tipo de cenário é realmente uma idéia boa ou razoável forçar a coleta de lixo?

Não estou pedindo casos específicos de C #, mas todas as linguagens de programação que possuem um coletor de lixo. Eu sei que você não pode forçar o GC em todas as linguagens, como Java, mas vamos supor que sim.


17
"mas todas as linguagens de programação que possuem um coletor de lixo". Diferentes idiomas (ou, mais apropriadamente, implementações diferentes ) usam métodos diferentes para a coleta de lixo, portanto, é improvável que você encontre uma regra de tamanho único.
Coronel Trinta e Dois

4
@Doval Se você estiver com restrições em tempo real e o GC não fornecer garantias correspondentes, estará entre uma pedra e um lugar difícil. Isso pode reduzir pausas indesejadas e não fazer nada, mas pelo que ouvi dizer, é "mais fácil" evitar a alocação no curso normal da operação.

3
Fiquei com a impressão de que, se você esperava prazos rígidos em tempo real, nunca usaria um idioma de GC.
GregRos

4
Não vejo como você pode responder a isso de uma maneira não específica da VM. Relevante para processos de 32 bits, sem relevância para processos de 64 bits. JVM do .NET e para o high-end
rwong 18/03/2015

3
@ DavidConrad você pode forçá-lo em c #. Daí a questão.
Omega

Respostas:


127

Você realmente não pode fazer declarações gerais sobre a maneira apropriada de usar todas as implementações de GC. Eles variam muito. Então, falarei com o .NET ao qual você se referiu originalmente.

Você deve conhecer o comportamento do GC intimamente para fazer isso com qualquer lógica ou razão.

O único conselho sobre coleção que posso dar é: nunca faça isso.

Se você realmente conhece os intrincados detalhes do GC, não precisará de meus conselhos para que isso não importe. Se você ainda não sabe com 100% de confiança, isso ajudará e terá que procurar on-line e encontrar uma resposta como esta: Você não deve ligar para o GC.Collect ou, alternativamente: você deve aprender os detalhes de como o GC funciona por dentro e por fora, e só então você saberá a resposta .

Há um lugar seguro que faz algum sentido usar o GC .

GC.Collect é uma API disponível que você pode usar para determinar o tempo das coisas. Você poderia criar um perfil de um algoritmo, coletar e criar outro algoritmo imediatamente depois, sabendo que o GC do primeiro algo não estava ocorrendo durante o segundo distorcendo os resultados.

Esse tipo de criação de perfil é a única vez que eu sugeriria coletar manualmente para qualquer pessoa.


De qualquer maneira, exemplo planejado

Um caso de uso possível é que, se você carregar coisas realmente grandes, elas acabarão no Heap de Objetos Grandes, que vai direto para a Geração 2, embora novamente a Geração 2 seja para objetos de vida longa, porque são coletados com menos frequência. Se você sabe que está carregando objetos de vida curta na Geração 2 por qualquer motivo, pode limpá-los mais rapidamente para manter sua Geração 2 menor e suas coleções mais rápidas.

Este é o melhor exemplo que eu poderia apresentar, e não é bom - a pressão do LOH que você está construindo aqui causaria coleções mais frequentes, e as coleções são tão frequentes quanto são - é provável que ele esteja limpando o LOH da mesma forma que rápido como você estava soprando com objetos temporários. Simplesmente não confio em mim mesmo para presumir uma frequência de coleta melhor do que o próprio GC - sintonizado por pessoas muito mais inteligentes que eu.


Então, vamos falar sobre algumas das semânticas e mecanismos no .NET GC ... ou ..

Tudo o que acho que sei sobre o .NET GC

Por favor, quem encontrar erros aqui - me corrija. Sabe-se que grande parte do GC é magia negra e, enquanto eu tentava deixar de fora os detalhes dos quais não tinha certeza, provavelmente ainda entendi algumas coisas erradas.

Abaixo, propositalmente, faltam vários detalhes dos quais não tenho certeza, além de um conjunto de informações muito maior do que simplesmente não conheço. Use essa informação por sua conta e risco.


Conceitos de GC

O GC do .NET ocorre em momentos inconsistentes, e é por isso que é chamado de "não determinístico"; isso significa que você não pode confiar nele para ocorrer em horários específicos. Também é um coletor de lixo de gerações, o que significa que ele particiona seus objetos em quantas passagens do GC eles passaram.

Os objetos no heap da Geração 0 passaram por 0 coleções, e foram criados recentemente; nenhuma coleção ocorreu desde a instanciação. Os objetos em seu heap Gen 1 passaram por uma passagem de coleta e da mesma forma os objetos em seu heap Gen 2 passaram por duas passagens de coleção.

Agora vale a pena notar o motivo pelo qual qualifica essas gerações e partições específicas de acordo. O .NET GC reconhece apenas essas três gerações, porque as passagens de coleta que passam por esses três montões são ligeiramente diferentes. Alguns objetos podem sobreviver a passes de coleta milhares de vezes. O GC apenas os deixa do outro lado da partição de heap da Geração 2, não há sentido em particioná-los em nenhum outro lugar porque na verdade são a Geração 44; a passagem de coleta neles é igual a tudo na pilha da geração 2.

Existem propósitos semânticos para essas gerações específicas, bem como mecanismos implementados que os honram, e chegarei a eles daqui a pouco.


O que há em uma coleção

O conceito básico de uma passagem de coleta de GC é que ele verifica cada objeto em um espaço de heap para ver se ainda existem referências ativas (raízes do GC) para esses objetos. Se uma raiz de GC for encontrada para um objeto, isso significa que atualmente o código em execução ainda pode alcançar e usar esse objeto e, portanto, não pode ser excluído. No entanto, se uma raiz do GC não for encontrada para um objeto, isso significa que o processo em execução não precisa mais do objeto, portanto, ele pode ser removido para liberar memória para novos objetos.

Agora, depois de terminar de limpar um monte de objetos e deixar alguns em paz, haverá um efeito colateral lamentável: lacunas de espaço livre entre objetos vivos, onde os mortos foram removidos. Essa fragmentação de memória, se deixada sozinha, simplesmente desperdiçaria memória; portanto, as coleções normalmente fazem o que é chamado de "compactação", onde pegam todos os objetos ativos restantes e os juntam no heap para que a memória livre seja contígua em um lado do heap para Gen 0

Agora, dada a idéia de três montes de memória, todos particionados pelo número de passagens de coleção pelas quais eles passaram, vamos falar sobre por que essas partições existem.


Coleção Gen 0

A geração 0, que é o mais novo objeto absoluto, tende a ser muito pequena - para que você possa coletá-lo com segurança com muita frequência . A frequência garante que o heap permaneça pequeno e que as coleções sejam muito rápidas porque estão sendo coletadas sobre um heap tão pequeno. Isso se baseia mais ou menos em uma heurística que afirma: A grande maioria dos objetos temporários criados por você é muito temporária; portanto, temporários, eles não serão mais usados ​​ou referenciados quase imediatamente após o uso e, portanto, podem ser coletados.


Coleção Gen 1

Sendo a geração 1, objetos que não se enquadram nessa categoria muito temporária de objetos, ainda podem ter vida curta, porque ainda assim - uma grande parte dos objetos criados não é usada por muito tempo. Portanto, a geração 1 também coleta com bastante frequência, mantendo novamente a pilha pequena, para que as coleções sejam rápidas. No entanto, a suposição é menor de que seus objetos são temporários que a geração 0, portanto, ela é coletada com menos frequência que a geração 0

Eu direi que, francamente, não conheço os mecanismos técnicos que diferem entre o passe de coleta do Gen 0 e o do Gen 1, se houver algum outro além da frequência que eles coletam.


Coleção Gen 2

A geração 2 agora deve ser a mãe de todos os montões, certo? Bem, sim, isso é mais ou menos certo. É onde todos os seus objetos permanentes vivem - o objeto em que você Main()vive, por exemplo, e tudo o que faz Main()referência, porque esses serão enraizados até que você Main()retorne ao final do processo.

Dado que a geração 2 é um balde para basicamente tudo o que as outras gerações não conseguiram coletar, seus objetos são em grande parte permanentes ou duram no mínimo. Portanto, reconhecer muito pouco do que está na geração 2 será algo que pode ser coletado, não sendo necessário coletá-lo com frequência. Isso permite que sua coleção também seja mais lenta, pois é executada com muito menos frequência. Então é basicamente aqui que eles adotam todos os comportamentos extras para cenários estranhos, porque eles têm tempo para executá-los.


Pilha de Objetos Grandes

Um exemplo dos comportamentos extras da geração 2 é que ele também faz a coleção no heap de objetos grandes. Até agora, eu tenho falado inteiramente sobre o Small Object Heap, mas o tempo de execução do .NET aloca coisas de determinados tamanhos para um heap separado por causa do que me referi como compactação acima. A compactação requer mover objetos quando as coleções terminam no Small Object Heap. Se houver um objeto vivo de 10mb na geração 1, levará muito mais tempo para concluir a compactação após a coleta, tornando a coleção da geração 1 mais lenta. Para que o objeto 10mb seja alocado para o Large Object Heap e coletado durante a geração 2, que é executada com pouca frequência.


Finalização

Outro exemplo são objetos com finalizadores. Você coloca um finalizador em um objeto que faz referência a recursos além do escopo do .NETs GC (recursos não gerenciados). O finalizador é a única maneira que o GC exige que um recurso não gerenciado seja coletado - você implementa o finalizador para fazer a coleta / remoção / liberação manual do recurso não gerenciado para garantir que ele não vaze do seu processo. Quando o GC executar a finalização de seus objetos, sua implementação limpará o recurso não gerenciado, tornando o GC capaz de remover seu objeto sem arriscar um vazamento de recursos.

O mecanismo com o qual os finalizadores fazem isso é ser referenciado diretamente em uma fila de finalização. Quando o tempo de execução aloca um objeto com um finalizador, ele adiciona um ponteiro a ele na fila de finalização e bloqueia seu objeto (chamado de fixação) para que a compactação não o mova, o que quebraria a referência da fila de finalização. À medida que as passagens de coleta ocorrem, eventualmente, seu objeto não possui mais uma raiz de GC, mas a finalização deve ser executada antes de poder ser coletada. Portanto, quando o objeto estiver morto, a coleção moverá sua referência da fila de finalização e fará uma referência a ela no que é conhecido como fila "FReachable". Então a coleção continua. Em outro momento "não determinístico" no futuro, um thread separado conhecido como thread do Finalizador passará pela fila FReachable, executando os finalizadores para cada um dos objetos mencionados. Após terminar, a fila FReachable fica vazia e virou um pouco no cabeçalho de cada objeto que diz que não precisa de finalização (esse bit também pode ser invertido manualmente comGC.SuppressFinalizeo que é comum em Dispose()métodos), eu também suspeito que ele tenha desafixado os objetos, mas não me cite. A próxima coleção que aparecer em qualquer pilha deste objeto, finalmente a coletará. As coleções da geração 0 nem prestam atenção aos objetos com esse bit necessário para finalização, mas os promovem automaticamente, sem mesmo verificar sua raiz. Um objeto não enraizado que precise de finalização na geração 1 será lançado na FReachablefila, mas a coleção não fará mais nada com ela; portanto, ela entrará na geração 2. Dessa forma, todos os objetos que têm um finalizador e não GC.SuppressFinalizeserão coletados na geração 2.


4
@FlorianMargaine sim ... dizendo alguma coisa sobre "GC" em todas as implementações realmente não faz sentido ..
Jimmy Hoffa

10
tl; dr: use conjuntos de objetos.
Robert Harvey

5
tl; dr: Para tempo / criação de perfil, pode ser útil.
kutschkem

3
@Den depois de ler minha descrição acima sobre a mecânica (como eu a entendo), qual seria o benefício que você vê? Você limpa um grande número de objetos - no SOH (ou LOH?)? Você acabou de fazer com que outros threads pausassem nesta coleção? Essa coleção apenas promoveu o dobro de objetos para a geração 2 do que foi liberado? A coleção causou compactação no LOH (você a ativou?)? Quantos heaps de GC você possui e o seu GC está no modo servidor ou área de trabalho? GC é um iceberg de gelo, a traição está abaixo das águas. Apenas fique longe. Eu não sou inteligente o suficiente para coletar confortavelmente.
Jimmy Hoffa

4
@RobertHarvey Os pools de objetos também não são uma bala de prata. A geração 0 do coletor de lixo já é efetivamente um pool de objetos - geralmente é dimensionada para caber no menor nível de cache e, portanto, novos objetos geralmente são criados na memória que já está no cache. Seu pool de objetos agora está competindo com o berçário do GC pelo cache, e se a soma do berçário do GC e seu pool for maior que o cache, obviamente você terá erros de cache. E se você planeja usar o paralelismo agora, precisa reimplementar a sincronização e se preocupar com o compartilhamento falso.
Doval

68

Infelizmente, ninguém lá elabora quais são esses casos.

Vou dar alguns exemplos. Em suma, é raro forçar um GC é uma boa ideia, mas pode valer totalmente a pena. Esta resposta é da minha experiência com a literatura .NET e GC. Deve generalizar bem para outras plataformas (pelo menos aquelas que possuem um GC significativo).

  • Benchmarks de vários tipos. Você deseja um estado de heap gerenciado conhecido quando um benchmark é iniciado, para que o GC não seja acionado aleatoriamente durante os benchmarks. Quando você repete uma referência, deseja o mesmo número e quantidade de GC em cada repetição.
  • Liberação repentina de recursos. Por exemplo, fechando uma janela da GUI significativa ou atualizando um cache (e, assim, liberando o antigo conteúdo potencialmente grande do cache). O GC não pode detectar isso porque tudo o que você está fazendo é definir uma referência como nula. O fato de tornar órfão um gráfico inteiro de objetos não é facilmente detectável.
  • Liberação de recursos não gerenciados que vazaram . Isso nunca deve acontecer, é claro, mas vi casos em que uma biblioteca de terceiros vazava coisas (como objetos COM). Às vezes, o desenvolvedor é forçado a induzir uma coleção.
  • Aplicativos interativos, como jogos . Durante os jogos, os orçamentos de tempo são muito rígidos por quadro (60Hz => 16ms por quadro). Para evitar hickups, você precisa de uma estratégia para lidar com os GCs. Uma dessas estratégias é atrasar o máximo possível os GCs G2 e forçá-los em um momento oportuno, como uma tela de carregamento ou uma cena cortada. O GC não pode saber quando é o melhor momento.
  • Controle de latência em geral. Alguns aplicativos da Web desabilitam os GCs e executam periodicamente uma coleção G2 enquanto estão fora da rotação do balanceador de carga. Dessa forma, a latência G2 nunca é revelada ao usuário.

Se seu objetivo for a taxa de transferência, quanto mais raro o GC, melhor. Nesses casos, forçar uma coleção não pode ter um impacto positivo (exceto problemas bastante planejados, como aumentar a utilização do cache da CPU, removendo objetos mortos intercalados nos ativos). A coleta de lotes é mais eficiente para todos os colecionadores que eu conheço. Para aplicativos de produção com consumo de memória em estado estacionário, induzir um GC não ajuda.

Os exemplos dados acima visam a consistência e a limitação do uso de memória. Nesses casos, os GCs induzidos podem fazer sentido.

Parece haver uma ideia ampla de que o CG é uma entidade divina que induz uma coleção sempre que é realmente ideal fazê-lo. Nenhum GC que eu conheço é tão sofisticado e, de fato, é muito difícil ser ideal para o GC. O GC sabe menos do que o desenvolvedor. Suas heurísticas são baseadas em contadores de memória e coisas como taxa de coleta e assim por diante. As heurísticas geralmente são boas, mas não capturam mudanças repentinas no comportamento do aplicativo, como liberação de grandes quantidades de memória gerenciada. Também é cego para recursos não gerenciados e para requisitos de latência.

Observe que os custos do GC variam com o tamanho da pilha e o número de referências na pilha. Em uma pequena pilha, o custo pode ser muito pequeno. Vi taxas de coleta do G2 com o .NET 4.5 de 1-2 GB / s em um aplicativo de produção com tamanho de heap de 1 GB.


Para o caso de controle de latência, acho que, em vez de fazer isso periodicamente, você também pode fazer isso por necessidade (ou seja, quando o uso da memória cresce acima de um determinado limite).
Paŭlo Ebermann 19/03/2015

3
+1 para o penúltimo parágrafo. Algumas pessoas têm o mesmo sentimento em relação aos compiladores e rapidamente chamam quase qualquer coisa de "otimização prematura". Costumo dizer-lhes algo semelhante.
Honza Brabec

2
+1 para esse parágrafo também. Acho chocante que as pessoas pensem que um programa de computador escrito por outra pessoa deve necessariamente entender melhor as características de desempenho de seu programa do que elas.
Mehrdad

1
@HonzaBrabec O problema é o mesmo nos dois casos: se você acha que conhece melhor que o GC ou o compilador, é muito fácil se machucar. Se você realmente sabe mais, está otimizando apenas quando sabe que não é prematuro.
Svick

27

Como princípio geral, um coletor de lixo é coletado quando se depara com "pressão de memória", e é uma boa idéia não coletá-lo em outros momentos porque você pode causar problemas de desempenho ou até pausas perceptíveis na execução do programa. E, de fato, o primeiro ponto depende do segundo: para um coletor de lixo geracional, pelo menos, ele roda com mais eficiência, quanto maior a proporção de lixo para bons objetos, de modo a minimizar a quantidade de tempo gasto na pausa do programa , tem que procrastinar e deixar o lixo se acumular o máximo possível.

O momento apropriado para chamar manualmente o coletor de lixo é quando você termina de fazer algo que 1) provavelmente criou muito lixo e 2) espera que o usuário demore algum tempo e deixe o sistema sem resposta de qualquer forma. Um exemplo clássico é no final do carregamento de algo grande (um documento, um modelo, um novo nível etc.)


12

Uma coisa que ninguém mencionou é que, enquanto o GC do Windows é incrivelmente bom, o GC no Xbox é lixo (trocadilhos) .

Portanto, ao codificar um jogo XNA para rodar no XBox, é absolutamente crucial cronometrar a coleta de lixo para momentos oportunos, ou você terá horríveis soluços intermitentes no FPS. Além disso, no XBox, é comum usar o structs way, com mais frequência do que você normalmente faria, para minimizar o número de objetos que precisam ser coletados no lixo.


4

A coleta de lixo é, acima de tudo, uma ferramenta de gerenciamento de memória. Como tal, os coletores de lixo serão coletados quando houver pressão de memória.

Os coletores de lixo modernos são muito bons e estão melhorando; portanto, é improvável que você possa aprimorá-los coletando manualmente. Mesmo que você possa melhorar as coisas hoje, pode ser que uma melhoria futura no seu coletor de lixo escolhido torne sua otimização ineficaz ou até contraproducente.

No entanto , os coletores de lixo normalmente não tentam otimizar o uso de recursos que não sejam a memória. Em ambientes de coleta de lixo, os recursos mais valiosos que não são de memória têm um closemétodo ou similar, mas há algumas ocasiões em que esse não é o caso por algum motivo, como compatibilidade com uma API existente.

Nesses casos, pode fazer sentido chamar manualmente a coleta de lixo quando você souber que um recurso valioso que não está em memória está sendo usado.

RMI

Um exemplo concreto disso é a Remote Method Invocation do Java. RMI é uma biblioteca de chamadas de procedimento remoto. Você normalmente tem um servidor, que disponibiliza vários objetos para uso dos clientes. Se um servidor souber que um objeto não está sendo usado por nenhum cliente, esse objeto estará qualificado para a coleta de lixo.

No entanto, a única maneira que o servidor sabe disso é se o cliente informar, e o cliente informará ao servidor que não precisa mais de um objeto depois que o cliente tiver coletado o lixo independentemente do que estiver usando.

Isso apresenta um problema, pois o cliente pode ter muita memória livre e, portanto, pode não executar a coleta de lixo com muita frequência. Enquanto isso, o servidor pode ter muitos objetos não utilizados na memória, que não podem ser coletados porque não sabem que o cliente não os está usando.

A solução no RMI é que o cliente execute a coleta de lixo periodicamente, mesmo quando houver muita memória livre, para garantir que os objetos sejam coletados imediatamente no servidor.


"Nesses casos, pode fazer sentido chamar manualmente a coleta de lixo quando você souber que um recurso valioso que não está em memória está sendo usado" - se um recurso que não estiver em memória estiver sendo usado, você deverá usar um usingbloco ou chamar um Closemétodo para garantir que o recurso seja descartado o mais rápido possível. Confiar no GC para limpar recursos que não são de memória não é confiável e causa todos os tipos de problemas (principalmente nos arquivos que precisam ser bloqueados para acesso, para que possam ser abertos apenas uma vez).
Jules

E, como indicado na resposta, quando um closemétodo está disponível (ou o recurso pode ser usado com um usingbloco), essa é a abordagem correta. A resposta lida especificamente com os casos raros em que esses mecanismos não estão disponíveis.
James_pic

Minha opinião pessoal é que qualquer interface que gerencia um recurso que não seja de memória, mas não fornece um método close, é uma interface que não deve ser usada , porque não há como usá-lo de maneira confiável.
Jules

@ Jules Eu concordo, mas às vezes é inevitável. Às vezes, abstrações vazam, e usar uma abstração com vazamento é melhor do que não usar abstração. Às vezes, você precisa trabalhar com um código legado que exige promessas que você sabe que não pode cumprir. Sim, é raro e deve ser evitado, se possível, e há uma razão para que haja todos esses avisos em torno de forçar a coleta de lixo, mas essas situações surgem e o OP estava perguntando como são essas situações - o que eu respondi .
James_pic

2

A melhor prática é não forçar uma coleta de lixo na maioria dos casos. (Todos os sistemas nos quais trabalhei que forçaram a coleta de lixo, tiveram problemas sublinhados que, se resolvidos, eliminariam a necessidade de forçar a coleta de lixo e aceleraram bastante o sistema.)

Existem alguns casos em que você sabe mais sobre o uso da memória e o coletor de lixo. É improvável que isso ocorra em um aplicativo multiusuário ou em um serviço que esteja respondendo a mais de uma solicitação por vez.

No entanto, em algum processamento de tipo de lote, você conhece mais do que o GC. Por exemplo, considere uma aplicação que.

  • É fornecida uma lista de nomes de arquivos na linha de comando
  • Processa um único arquivo e depois grava o resultado em um arquivo de resultados.
  • Durante o processamento do arquivo, cria muitos objetos interligados que não podem ser coletados até que o processamento do arquivo seja concluído (por exemplo, uma árvore de análise)
  • Não mantém o estado de correspondência entre os arquivos processados .

Você poderá realizar um teste (após cuidadoso) de que você deve forçar uma coleta de lixo completa depois de processar cada arquivo.

Outros casos é um serviço que acorda a cada poucos minutos para processar alguns itens e não mantém nenhum estado enquanto dorme . Forçar uma coleção completa antes de dormir pode valer a pena.

A única vez em que consideraria forçar uma coleção é quando sei que muitos objetos foram criados recentemente e muito poucos objetos são atualmente referenciados.

Eu preferiria ter uma API de coleta de lixo quando pudesse dar dicas sobre esse tipo de coisa sem ter que forçar um GC sozinho.

Veja também " Boatos sobre o desempenho de Rico Mariani "


2

Existem vários casos em que você pode querer chamar gc ().

  • [ Algumas pessoas dizem que isso não é bom porque pode promover objetos para o espaço da geração mais antiga, o que eu concordo que não é uma coisa boa. No entanto, é não sempre verdade que sempre haverá objetos que podem ser promovidos. Certamente, é possível que, após essa gc()chamada, poucos objetos permaneçam e muito menos sejam movidos para o espaço da geração anterior ] Quando você criar uma grande coleção de objetos e usar muita memória. Você simplesmente deseja limpar o máximo de espaço possível. Isso é apenas senso comum. Ao chamar gc()manualmente, não haverá verificação redundante do gráfico de referência em parte dessa grande coleção de objetos que você está carregando na memória. Em resumo, se você executar gc()antes de carregar muito na memória, ogc() induzido durante a carga acontece menos em pelo menos uma vez quando o carregamento começa a criar pressão de memória.
  • Quando você terminar de carregar uma grande coleção degrandeobjetos e é improvável que você carregue mais objetos na memória. Em resumo, você passa da fase de criação para a fase de uso. Ao chamar, gc()dependendo da implementação, a memória usada será compactada, o que melhora enormemente a localidade do cache. Isso resultará em grande melhoria no desempenho que você não obterá da criação de perfil .
  • Semelhante ao primeiro, mas da visão de que, se você o fizer gc()e a implementação do gerenciamento de memória suportar, você criará uma continuidade muito melhor para sua memória física. Isso novamente torna a nova grande coleção de objetos mais contínua e compacta, o que, por sua vez, melhora o desempenho

1
Alguém pode apontar o motivo do voto negativo? Eu mesmo não sei o suficiente para julgar a resposta (à primeira vista, isso meio que faz sentido para mim).
Omega

1
Acho que você conseguiu uma votação negativa pelo terceiro ponto. Potencialmente também por dizer "Isso é apenas senso comum".
immibis 18/03/2015

2
Quando você cria uma grande coleção de objetos, o GC deve ser inteligente o suficiente para saber se uma coleção é necessária. Mesmo quando a memória precisa ser compactada. Confiar no GC para otimizar a localização da memória de objetos relacionados não parece confiável. Eu acho que você pode encontrar outras soluções (struct, inseguro, ...). (Eu não sou o derrotador).
Guillaume

3
Sua primeira idéia de uma hora certa é apenas um mau conselho na minha opinião. As chances são altas de que houve uma coleção recentemente; portanto, sua tentativa de coletar novamente simplesmente promoverá arbitrariamente objetos para as gerações posteriores, o que quase sempre é ruim. Gerações posteriores têm coleções que demoram mais para começar, aumentando seus tamanhos de heap "para liberar o máximo de espaço possível" apenas fazendo com que isso seja mais problemático. Além disso, se você está prestes a aumentar a pressão de memória com uma carga, é provável que você começar a induzir coleções de qualquer maneira, que será executado de forma mais lenta porque o aumento da Gen1 / 2
Jimmy Hoffa

2
By calling gc() depending on implementation, the memory in used will be compacted which massively improves cache locality. This will result in massive improve in performance that you will not get from profiling.Se você alocar uma tonelada de objetos em uma linha, as probabilidades já estão compactadas. Se houver, a coleta de lixo pode embaralhá-los um pouco. De qualquer maneira, o uso de estruturas de dados densas e que não pulam aleatoriamente na memória terá um impacto maior. Se você estiver usando uma lista vinculada de um elemento por nó ingênua, nenhuma quantidade de truques manuais do GC compensará isso.
Doval

2

Um exemplo do mundo real:

Eu tinha um aplicativo Web que fazia uso de um conjunto muito grande de dados que raramente mudavam e que precisavam ser acessados ​​muito rapidamente (rápido o suficiente para resposta por pressionamento de tecla via AJAX).

O óbvio o suficiente para fazer aqui é carregar o gráfico relevante na memória e acessá-lo a partir daí, e não no banco de dados, atualizando o gráfico quando o banco de dados é alterado.

Mas, sendo muito grande, uma carga ingênua ocuparia pelo menos 6 GB de memória com os dados, devido ao crescimento no futuro. (Eu não tenho números exatos, uma vez que ficou claro que minha máquina de 2 GB estava tentando lidar com pelo menos 6 GB, eu tinha todas as medidas necessárias para saber que não iria funcionar).

Felizmente, havia um grande número de objetos imutáveis ​​em picolé nesse conjunto de dados que eram iguais entre si; depois que eu descobrisse que um determinado lote era o mesmo que outro lote, eu poderia usar uma referência ao outro, permitindo que muitos dados fossem coletados e, portanto, encaixando tudo em menos de meio show.

Tudo bem, mas para isso ainda agitamos mais de 6 GB de objetos no espaço de meio minuto para chegar a esse estado. Deixada por conta própria, a GC não aguentou; o pico de atividade em relação ao padrão usual do aplicativo (muito menos pesado em desalocações por segundo) foi muito acentuado.

Portanto, chamar periodicamente GC.Collect()durante esse processo de compilação significava que tudo funcionava sem problemas. Obviamente, eu não liguei manualmente GC.Collect()o resto do tempo em que o aplicativo é executado.

Este caso do mundo real é um bom exemplo das diretrizes de quando devemos usar GC.Collect():

  1. Use com um caso relativamente raro de muitos objetos sendo disponibilizados para coleta (o valor de megabytes estava sendo disponibilizado e essa criação de gráficos era um caso muito raro durante a vida útil do aplicativo (cerca de um minuto por semana).
  2. Faça-o quando uma perda de desempenho for relativamente tolerável; isso aconteceu apenas na inicialização do aplicativo. (Outro bom exemplo dessa regra é entre níveis durante um jogo ou outros pontos em um jogo em que os jogadores não ficam chateados nem por uma pausa).
  3. Perfil para garantir que realmente haja uma melhoria. (Muito fácil; "Funciona" quase sempre bate "não funciona").

Na maioria das vezes, quando pensei que poderia ter um caso em que GC.Collect()valha a pena chamar, porque os pontos 1 e 2 se aplicavam, o ponto 3 sugeria que piorava as coisas ou pelo menos não melhorava (e com pouca ou nenhuma melhoria eu incline-se para não chamar a chamada, pois a abordagem tem mais chances de ser melhor durante a vida útil de um aplicativo).


0

Eu tenho um uso para a eliminação de lixo que é um pouco heterodoxo.

Existe essa prática equivocada que infelizmente é muito prevalente no mundo C #, de implementar o descarte de objetos usando o idioma feio, desajeitado, deselegante e suscetível a erros conhecido como descarte descartável . O MSDN a descreve detalhadamente , e muitas pessoas juram, seguem religiosamente, passam horas e horas discutindo precisamente como isso deve ser feito etc.

(Observe que o que estou chamando de feio aqui não é o padrão de descarte de objetos; o que estou chamando de feio é o IDisposable.Dispose( bool disposing )idioma específico .)

Esse idioma foi inventado porque é supostamente impossível garantir que o destruidor de seus objetos sempre seja invocado pelo coletor de lixo para limpar os recursos, para que as pessoas realizem a limpeza de recursos por dentro IDisposable.Dispose()e, caso se esqueçam, também tentam mais uma vez. dentro do destruidor. Você sabe, apenas por precaução.

Mas então você IDisposable.Dispose()pode ter objetos gerenciados e não gerenciados para limpar, mas os objetos gerenciados não podem ser limpos quando IDisposable.Dispose()são invocados de dentro do destruidor, porque eles já foram tratados pelo coletor de lixo naquele momento. é essa a necessidade de um Dispose()método separado que aceite um bool disposingsinalizador para saber se os objetos gerenciados e não gerenciados devem ser limpos ou apenas os não gerenciados.

Com licença, mas isso é insano.

Eu passo pelo axioma de Einstein, que diz que as coisas devem ser o mais simples possível, mas não mais simples. Claramente, não podemos omitir a limpeza de recursos; portanto, a solução mais simples possível deve incluir pelo menos isso. A próxima solução mais simples envolve sempre descartar tudo no momento exato em que ele deve ser descartado, sem complicar as coisas, contando com o destruidor como alternativa.

Agora, estritamente falando, é obviamente impossível garantir que nenhum programador cometa o erro de esquecer de invocar IDisposable.Dispose(), mas o que podemos fazer é usar o destruidor para detectar esse erro. Na verdade, é muito simples: tudo o que o destruidor precisa fazer é gerar uma entrada de log se detectar que o disposedsinalizador do objeto descartável nunca foi definido true. Portanto, o uso do destruidor não é parte integrante de nossa estratégia de descarte, mas é nosso mecanismo de garantia de qualidade. E como esse é um teste apenas no modo de depuração, podemos colocar todo o destruidor dentro de um #if DEBUGbloco, para nunca incorrer em nenhuma penalidade de destruição em um ambiente de produção. (O IDisposable.Dispose( bool disposing )idioma prescreve queGC.SuppressFinalize() deve ser invocado precisamente para diminuir a sobrecarga da finalização, mas com o meu mecanismo é possível evitar completamente essa sobrecarga no ambiente de produção.)

O que se resume é o eterno erro rígido vs. argumento de erro leve : o IDisposable.Dispose( bool disposing )idioma é uma abordagem de erro suave e representa uma tentativa de permitir que o programador esqueça de invocar Dispose()sem que o sistema falhe, se possível. A abordagem de erro rígido diz que o programador deve sempre garantir que isso Dispose()será invocado. A penalidade geralmente prescrita pela abordagem de erro grave na maioria dos casos é falha de asserção, mas nesse caso específico, fazemos uma exceção e diminuímos a penalidade para uma simples emissão de uma entrada de log de erro.

Portanto, para que esse mecanismo funcione, a versão DEBUG do nosso aplicativo deve executar um descarte de lixo completo antes de sair, para garantir que todos os destruidores serão chamados e, assim, capturar quaisquer IDisposableobjetos que esquecemos de descartar.


Now, strictly speaking, it is of course impossible to guarantee that no programmer will ever make the mistake of forgetting to invoke IDisposable.Dispose()Na verdade, não é, embora eu não ache que o C # seja capaz disso. Não exponha o recurso; em vez disso, forneça uma DSL para descrever tudo o que você fará com ela (basicamente uma mônada), além de uma função que adquira o recurso, faça as coisas, libere e retorne o resultado. O truque é usar o sistema de tipos para garantir que, se alguém contrabandear uma referência para o recurso, ele não possa ser usado em outra chamada para a função de execução.
Doval

2
O problema com Dispose(bool disposing)(que não está definido IDisposableé que ele é usado para lidar com a limpeza de objetos gerenciados e não gerenciados) que o objeto possui como um campo (ou é responsável por), que está resolvendo o problema errado. objetos não gerenciados em um objeto gerenciado sem outros objetos descartáveis ​​com os quais se preocupar, todos os Dispose()métodos serão um deles (faça com que o finalizador faça a mesma limpeza, se necessário) ou apenas com objetos gerenciados para serem descartados (não tenha um finalizador em tudo), ea necessidade de bool disposingdesaparecer.
Jon Hanna

-1 conselhos ruins por causa de como a finalização realmente funciona. Eu concordo totalmente com o seu ponto de vista sobre o dispose(disposing)idioma ser terribad, mas digo isso porque as pessoas costumam usar essa técnica e finalizadores quando eles só têm recursos gerenciados (o DbConnectionobjeto, por exemplo, é gerenciado , não é pontilhado ou com empacotamento), e VOCÊ DEVE SOMENTE NUNCA IMPLEMENTA UM FINALIZADOR COM CÓDIGO NÃO GERENCIADO, PINVOKED, COM MARSHALLED OU NÃO SEGURO . Eu detalhei acima em minha resposta como os finalizadores são extremamente caros, não os use a menos que você tenha recursos não gerenciados em sua classe.
Jimmy Hoffa

2
Eu quase quero dar +1 a você, apenas porque você está decifrando algo que muitas pessoas consideram uma coisa importante no dispose(dispoing)idioma, mas a verdade é que isso é tão predominante porque as pessoas têm tanto medo de coisas do GC que algo tão não relacionado como isso ( disposedeveria ter nada a ver com GC) merece que eles tomem apenas o medicamento prescrito sem sequer investigá-lo. Bom para você para verificá-lo, mas você perdeu o maior inteiro (que incentiva finalizadores FARRR mais frequentemente do que deveria ser)
Jimmy Hoffa

1
@JimmyHoffa obrigado pela sua contribuição. Concordo que um finalizador normalmente deve ser usado apenas para liberar recursos não gerenciados, mas você não concorda que na compilação DEBUG essa regra é inaplicável e que na compilação DEBUG devemos usar livremente finalizadores para detectar bugs? É tudo o que estou sugerindo aqui, então não vejo por que você discorda. Consulte também programmers.stackexchange.com/questions/288715/… para obter uma explicação mais longa dessa abordagem no lado java do mundo.
precisa

0

Você pode me dizer em que tipo de cenário é realmente uma idéia boa ou razoável forçar a coleta de lixo? Não estou pedindo casos específicos de C #, mas todas as linguagens de programação que possuem um coletor de lixo. Eu sei que você não pode forçar o GC em todas as linguagens, como Java, mas vamos supor que sim.

Falando muito teoricamente e desconsiderando questões como algumas implementações de GC que diminuem a velocidade durante seus ciclos de coleta, o maior cenário em que consigo pensar em forçar a coleta de lixo é um software de missão crítica, onde os vazamentos lógicos são preferíveis aos travamentos de ponteiros pendurados, por exemplo, porque travam em momentos inesperados pode custar vidas humanas ou algo desse tipo.

Se você olhar para alguns dos jogos indie mais ruins escritos usando linguagens da GC, como jogos em Flash, eles vazam como loucos, mas não travam. Eles podem levar dez vezes a memória em 20 minutos para jogar o jogo, porque parte da base de código do jogo esqueceu de definir uma referência como nula ou removê-la de uma lista, e as taxas de quadros podem começar a sofrer, mas o jogo ainda funciona. Um jogo semelhante escrito com códigos de má qualidade C ou C ++ pode falhar como resultado do acesso a ponteiros pendentes como resultado do mesmo tipo de erro de gerenciamento de recursos, mas não vazaria tanto.

Para jogos, a falha pode ser preferível no sentido de que pode ser rapidamente detectada e corrigida, mas para um programa de missão crítica, falhas em momentos totalmente inesperados podem matar alguém. Portanto, os principais casos que acho que seriam cenários em que não há falhas ou outras formas de segurança são absolutamente críticos, e um vazamento lógico é uma coisa relativamente trivial em comparação.

O cenário principal em que acho ruim forçar o GC é para coisas em que o vazamento lógico é realmente menos preferível do que uma falha. Nos jogos, por exemplo, a falha não mata necessariamente ninguém e pode ser facilmente capturada e corrigida durante os testes internos, enquanto um vazamento lógico pode passar despercebido mesmo depois que o produto é lançado, a menos que seja tão grave que torne o jogo impossível de ser jogado em questão de minutos. . Em alguns domínios, uma falha facilmente reproduzível que ocorre nos testes às vezes é preferível a um vazamento que ninguém nota imediatamente.

Outro caso em que posso pensar em que seria preferível forçar o GC a uma equipe é para um programa de vida muito curta, como apenas algo executado na linha de comando que executa uma tarefa e depois é encerrado. Nesse caso, a vida útil do programa é muito curta para tornar qualquer tipo de vazamento lógico não trivial. Vazamentos lógicos, mesmo para grandes recursos, geralmente só se tornam problemáticos horas ou minutos após a execução do software; portanto, é improvável que um software que seja executado apenas por 3 segundos tenha problemas com vazamentos lógicos, o que poderia causar muitas coisas. É mais fácil escrever programas de curta duração se a equipe acabou de usar o GC.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.