Excluir C ++ vs Java GC

8

A coleta de lixo do Java cuida de objetos mortos na pilha, mas congela o mundo algumas vezes. Em C ++, tenho que ligar deletepara descartar um objeto criado no final de seu ciclo de vida.

Este deleteparece ser um preço muito baixo para pagar por ambiente não-congelamento. A colocação de todas as deletepalavras-chave relevantes é uma tarefa mecânica. Pode-se escrever um script que viaje pelo código e exclua uma vez que nenhuma nova ramificação use um determinado objeto.

Portanto, quais são os prós e os contras do Java build in vs C ++ diy model of garbage collection.


Não quero iniciar um thread C ++ vs Java. Minha pergunta é diferente.
Essa coisa toda do GC - tudo se resume a "apenas ser elegante, não se esqueça de excluir objetos que você criou - e você não precisará de nenhum GC dedicado? Ou é mais parecido com" descartar objetos em C ++ é realmente complicado - eu gastar 20% do meu tempo nisso e, no entanto, vazamentos de memória são um lugar comum "?

sixtytrees
fonte
15
Acho difícil acreditar que seu roteiro mágico possa existir. Além de tudo, não teria que conter uma solução para o problema da parada. Ou, em alternativa (e mais provável) só funcionam para programas incrivelmente simples
Richard Tingle
1
Também java moderno não congelar, exceto sob condições extremas, onde grandes quantidades de lixo são criados
Richard Tingle
1
@ThomasCarlisle bem, pode - e ocasionalmente afeta - afetar o desempenho. Mas existem muitos parâmetros para ajustar nesses casos, e às vezes a solução pode ser mudar completamente para um gc diferente. Tudo depende da quantidade de recursos disponíveis e da carga típica.
Hulk
4
"A colocação de todas as palavras-chave de exclusão relevantes é uma tarefa mecânica " - é por isso que existem ferramentas para detectar vazamentos de memória. Porque é tão simples e não propenso a erros.
JensG
2
@Doval, se você estiver usando o RAII corretamente, o que é bastante simples, não há quase nenhuma contabilidade. newentra no construtor da sua classe que gerencia a memória, deleteentra no destruidor. A partir daí, é tudo automático (armazenamento). Note que isso funciona para todos os tipos de recursos, não apenas para memória, ao contrário da coleta de lixo. Mutexes são obtidos em um construtor, liberados em um destruidor. Os arquivos são abertos em um construtor, fechados em um destruidor.
Rob K

Respostas:

18

O ciclo de vida do objeto C ++

Se você criar objetos locais, não precisará excluí-los: o compilador gera código para excluí-los automaticamente quando o objeto sai do escopo

Se você usar ponteiros de objetos e criar objetos no armazenamento gratuito, será necessário excluir o objeto quando ele não for mais necessário (como você descreveu). Infelizmente, em softwares complexos, isso pode ser muito mais desafiador do que parece (por exemplo, e se uma exceção for levantada e a parte de exclusão esperada nunca for alcançada?).

Felizmente, no C ++ moderno (também conhecido como C ++ 11 e posterior), você tem indicadores inteligentes, como por exemplo shared_ptr. Eles fazem a contagem de referência do objeto criado de uma maneira muito eficiente, um pouco como um coletor de lixo faria em Java. E assim que o objeto não for mais referenciado, o último ativo shared_ptrexcluirá o objeto para você. Automaticamente. Como um coletor de lixo, mas um objeto de cada vez e sem demora (Ok: você precisa de alguns cuidados extras e weak_ptrlidar com referências circulares).

Conclusão: hoje em dia você pode escrever código C ++ sem se preocupar com a alocação de memória e que é tão livre de vazamentos quanto com um GC, mas sem o efeito de congelamento.

O ciclo de vida do objeto Java

O bom é que você não precisa se preocupar com o ciclo de vida do objeto. Você apenas os cria e o java cuida do resto. Um GC moderno identificará e destruirá os objetos que não são mais necessários (inclusive se houver referências circulares entre objetos mortos).

Infelizmente, devido a esse conforto, você não tem controle real de quando o objeto é realmente excluído . Semanticamente, a exclusão / destruição coincide com a coleta de lixo .

Isso é perfeitamente bom se você olhar objetos apenas em termos de memória. Exceto pelo congelamento, mas estes não são fatais (as pessoas estão trabalhando nisso). Eu não sou especialista em Java, mas acho que a destruição atrasada torna mais difícil identificar vazamentos em java devido a referências mantidas acidentalmente, apesar de os objetos não serem mais necessários (ou seja, você não pode realmente monitorar a exclusão de objetos).

Mas e se o objeto tiver que controlar outros recursos além da memória, por exemplo, um arquivo aberto, um semáforo, um serviço do sistema? Sua classe deve fornecer um método para liberar esses recursos. E você terá a responsabilidade de garantir que esse método seja chamado quando os recursos não forem mais necessários. Em todo caminho de ramificação possível através do seu código, garantindo que ele também seja chamado em caso de exceções. O desafio é muito semelhante à exclusão explícita em C ++.

Conclusão: o GC resolve um problema de gerenciamento de memória. Mas não trata do gerenciamento de outros recursos do sistema. A ausência de exclusão "just-in-time" pode tornar o gerenciamento de recursos muito desafiador.

Exclusão, coleta de lixo e RAII

Quando você pode controlar a exclusão de um objeto e o destruidor que deve ser chamado na exclusão, você pode se beneficiar da RAII . Essa abordagem vê a memória apenas como um caso especial de alocação de recursos e vincula o gerenciamento de recursos com mais segurança ao ciclo de vida do objeto, garantindo assim o uso estritamente controlado dos recursos.

Christophe
fonte
3
A beleza dos coletores de lixo (modernos) é que você realmente não precisa pensar em referências cíclicas. Esses grupos de objetos inacessíveis, exceto um pelo outro, são detectados e coletados. Essa é uma enorme vantagem sobre simples contagem de referência / ponteiros inteligentes.
Hulk
6
+1 A parte "exceções" não pode ser estressada o suficiente. A presença de exceções torna a tarefa difícil e sem sentido do gerenciamento manual de memória praticamente impossível e, portanto, o gerenciamento manual de memória não é usado no C ++. Use RAII. Não use newfora dos construtores / ponteiros inteligentes e nunca use deletefora de um destruidor.
Felix Dombek
@ Hulk Você tem um ponto aqui! Embora a maioria dos meus argumentos ainda seja válida, a GC fez muitos progressos. E as referências circulares são realmente difíceis de manipular apenas com a contagem de referências de indicadores inteligentes. Então, editei minha resposta de acordo, para manter um equilíbrio melhor. Também adicionei uma referência a um artigo sobre possíveis estratégias para mitigar o efeito de congelamento.
Christophe
3
+1 O gerenciamento de outros recursos além da memória exige algum esforço extra para os idiomas do GC, porque o RAII não funciona se não houver controle refinado sobre quando (ou se) a exclusão / finalização de um objeto é feliz. Construções especiais estão disponíveis na maioria desses idiomas (consulte a tentativa com recursos de java , por exemplo), que pode, em combinação com os avisos do compilador, ajudar a corrigir essas coisas.
Hulk #
1
Like garbage collector, but one object at a time and without delay (Ok: you need some extra care and weak_ptr to cope with circular references).Contagem de referência pode cascata embora. Por exemplo, a última referência a Adesaparece, mas Atambém tem a última referência a B, quem tem a última referência a C... And you'll have the responsibility to make sure that this method is called when the resources are no longer needed. In every possible branching path through your code, ensuring it is also invoked in case of exceptions.Java e C # têm instruções de bloco especiais para isso.
Doval
5

Essa exclusão parece um preço muito baixo a pagar pelo ambiente sem congelamento. A colocação de todas as palavras-chave de exclusão relevantes é uma tarefa mecânica. Pode-se escrever um script que viaje pelo código e exclua uma vez que nenhuma nova ramificação use um determinado objeto.

Se você pode escrever um roteiro como esse, parabéns. Você é um desenvolvedor melhor do que eu. De longe.

A única maneira de você realmente evitar vazamentos de memória em casos práticos é padrões de codificação muito rígidos, com regras muito rígidas, que são as proprietárias de um objeto, e quando ele pode e deve ser liberado, ou ferramentas como ponteiros inteligentes que contam referências a objetos e excluem objetos quando a última referência se foi.

gnasher729
fonte
4
Sim, o gerenciamento manual de recursos é sempre suscetível a erros; felizmente, é necessário apenas em Java (para recursos que não sejam de memória), pois o C ++ possui RAII.
Deduplicator
A única maneira de realmente evitar QUALQUER vazamento de recurso em casos práticos é padrões de codificação muito rígidos, com regras muito rígidas, que é o proprietário de um objeto ... a memória não é o único recurso e, na maioria dos casos, não o mais crítico ou.
precisa
2
Se você considera o RAII um "padrão de codificação muito rigoroso". Considero um "poço de sucesso", trivialmente fácil de usar.
Rob K
5

Se você escreve o código C ++ correto com RAII, normalmente não escreve nenhum novo ou exclui. Os únicos "novos" que você escreve estão dentro de ponteiros compartilhados, para que você realmente nunca precise usar "excluir".

Nikko
fonte
1
Você não deveria estar usando new, mesmo com ponteiros compartilhados - você deveria estar usando std::make_shared.
Jules
1
ou melhor make_unique. É realmente muito raro que você realmente precise de propriedade compartilhada.
Marc
4

Facilitar a vida dos programadores e evitar vazamentos de memória é uma vantagem importante da coleta de lixo, mas não é a única. Outra é impedir a fragmentação da memória. No C ++, depois de alocar um objeto usando a newpalavra-chave, ele permanece em uma posição fixa na memória. Isso significa que, conforme o aplicativo é executado, você acaba tendo lacunas de memória livre entre os objetos alocados. Portanto, a alocação de memória no C ++ deve, necessariamente, ser um processo mais complicado, pois o sistema operacional precisa encontrar blocos não alocados de determinado tamanho que se ajustem às lacunas.

A coleta de lixo cuida disso, pegando todos os objetos que não são excluídos e deslocando-os na memória para formar um bloco contínuo. Se você perceber que a coleta de lixo leva algum tempo, provavelmente é por causa desse processo, não devido à desalocação de memória. O benefício disso é que, quando se trata de alocação de memória, é quase tão simples quanto mudar um ponteiro para o final da pilha.

Portanto, em C ++, a exclusão de objetos é rápida, mas sua criação pode ser lenta. No Java, a criação de objetos não leva tempo, mas você precisa fazer algumas tarefas de limpeza de vez em quando.

kamilk
fonte
4
Sim, a alocação do armazenamento gratuito é mais lenta em C ++ que em Java. Felizmente, é muito menos frequente e você pode facilmente usar alocadores para fins especiais, a seu critério, onde houver padrões incomuns. Além disso, todos os recursos são iguais em C ++, Java possui casos especiais.
Deduplicator
3
Em 20 anos de codificação em C ++, nunca vi a fragmentação de memória se tornar um problema. Sistemas operacionais modernos com gerenciamento de memória virtual em processadores com vários níveis de cache eliminaram esse problema em grande parte.
Rob K
@RobK - "Sistemas operacionais modernos com gerenciamento de memória virtual em processadores com vários níveis de cache eliminaram amplamente [fragmentação] como um problema" - não, eles não o fizeram. Você pode não perceber, mas ainda acontece, ainda causa (1) perda de memória e (2) uso menos eficiente de caches, e as únicas soluções viáveis ​​são copiar o GC ou um design muito cuidadoso do gerenciamento manual de memória para garantir que não ocorra. isso acontece.
Jules
3

As principais promessas de Java foram

  1. C compreensível como sintaxe
  2. Escreva uma corrida em qualquer lugar
  3. Facilitamos o seu trabalho - até cuidamos do lixo.

Parece que Java garante que o lixo será descartado (não necessariamente de maneira eficiente). Se você usa C / C ++, tem liberdade e responsabilidade. Você pode fazer isso melhor do que o GC do Java ou pode ser muito pior (pule deletetodos juntos e tenha problemas de vazamento de memória).

Se você precisar de um código que "atenda a certos padrões de qualidade" e para otimizar a "relação preço / qualidade", use Java. Se você estiver pronto para investir recursos extras (tempo de seus especialistas) para melhorar o desempenho de aplicativos de missão crítica - use C.

Ju Shua
fonte
Bem, todas as promessas podem ser vistas como quebradas, facilmente.
Deduplicator
Exceto que o único lixo que o Java coletará é a memória alocada dinamicamente. Não faz nada sobre qualquer outro recurso alocado dinamicamente.
Rob K
@ RobK - isso não é estritamente verdade. O uso de finalizadores de objetos pode lidar com a desalocação de outros recursos. É amplamente desencorajado porque na maioria dos casos você não deseja isso (ao contrário da memória, a maioria dos outros tipos de recursos é muito mais restrita ou até única e, portanto, a desalocação precisa é crítica), mas isso pode ser feito. Java também possui a instrução try-with-resources que pode ser usada para automatizar o gerenciamento de outros recursos (oferecendo benefícios semelhantes ao RAII).
Jules
@ Jules: Uma diferença fundamental é que os objetos C ++ se autodestruem. Em Java, objetos closeable não podem se fechar. Um desenvolvedor Java escrevendo "new Something (...)" ou (pior) Something s = SomeFactory.create (...) deve verificar se algo está próximo. Mudar uma classe de Não aproximado para Closeable é o pior tipo de mudança radical e, portanto, praticamente nunca pode ser feito. Não é tão ruim no nível da classe, mas um problema sério quando se está definindo uma interface Java.
Kevin cline
2

A grande diferença que a coleta de lixo faz não é que você não precise excluir objetos explicitamente. A diferença muito maior é que você não precisa copiar objetos.

Isso tem efeitos que se tornam difundidos no design de programas e interfaces em geral. Deixe-me dar apenas um pequeno exemplo para mostrar como isso é abrangente.

Em Java, quando você abre algo de uma pilha, o valor que está sendo exibido é retornado, para que você obtenha um código como este:

WhateverType value = myStack.Pop();

Em Java, isso é seguro para exceções, porque tudo o que realmente estamos fazendo é copiar uma referência a um objeto, que é garantido que isso aconteça sem uma exceção. O mesmo não é verdade em C ++. Em C ++, retornar um valor significa (ou pelo menos pode significar) copiar esse valor e com alguns tipos que podem gerar uma exceção. Se a exceção for lançada após o item ter sido removido da pilha, mas antes que a cópia chegue ao destinatário, o item vazou. Para evitar isso, a pilha do C ++ usa uma abordagem um pouco desajeitada, na qual recuperar o item superior e remover o item superior são duas operações separadas:

WhateverType value = myStack.top();
myStack.pop();

Se a primeira instrução gerar uma exceção, a segunda não será executada; portanto, se uma exceção for lançada na cópia, o item permanecerá na pilha como se nada tivesse acontecido.

O problema óbvio é que isso é simplesmente desajeitado e (para pessoas que não o usaram) inesperado.

O mesmo acontece em muitas outras partes do C ++: especialmente no código genérico, a segurança de exceção invade muitas partes do design - e isso se deve em grande parte ao fato de que pelo menos potencialmente envolve copiar objetos (que podem ser lançados), onde Java apenas criaria novas referências a objetos existentes (que não podem ser lançados, portanto, não precisamos nos preocupar com exceções).

Quanto a um script simples para inserir, deletequando necessário: se você pode determinar estaticamente quando excluir itens com base na estrutura do código-fonte, provavelmente não deveria estar usando newe, deleteem primeiro lugar.

Deixe-me dar um exemplo de um programa para o qual isso quase certamente não seria possível: um sistema para fazer, acompanhar, cobrar (etc.) telefonemas. Quando você disca o telefone, ele cria um objeto de "chamada". O objeto de chamada controla quem você ligou, por quanto tempo você conversa com eles, etc., para adicionar registros apropriados aos logs de cobrança. O objeto de chamada monitora o status do hardware; portanto, quando você desliga, ele se destrói (usando o amplamente discutido delete this;). Só que não é tão trivial quanto "quando você desliga". Por exemplo, você pode iniciar uma chamada em conferência, conectar duas pessoas e desligar - mas a chamada continua entre essas duas partes mesmo depois de desligar (mas o faturamento pode mudar).

Jerry Coffin
fonte
"Se a exceção for lançada após o item ter sido removido da pilha, mas antes que a cópia chegue ao destinatário, o item vazou" você tem alguma referência para isso? Porque isso parece extremamente estranho, embora eu não seja um especialista em c ++.
Esben Skov Pedersen
@EsbenSkovPedersen: O GoTW # 8 seria um ponto de partida razoável. Se você tiver acesso a ele, o C ++ excepcional possui um pouco mais. Observe que ambos esperam pelo menos algum conhecimento pré-existente de C ++.
Jerry Coffin
Isso parece bastante direto. Na verdade, é essa frase que me confunde "Em C ++, retornar um valor significa (ou pelo menos pode significar) copiar esse valor e com alguns tipos que podem gerar uma exceção" Esta cópia está na pilha ou na pilha?
Esben Skov Pedersen
Você absolutamente não precisa copiar objetos em C ++, mas pode fazê-lo, ao contrário das linguagens de coleta de lixo (Java, C #), que tornam uma PITA copiar um objeto quando desejar. 90% dos objetos que eu criar devem ser destruídos e seus recursos liberados quando ficarem fora do escopo. Para forçar todos os objetos ao armazenamento dinâmico, porque 10% deles precisam parecer tolos na melhor das hipóteses.
Rob K
2
Porém, isso não é realmente uma diferença causada pelo uso da coleta de lixo, mas pela filosofia simplificada de "todos os objetos são referências" do Java. Considere o C # como um contra-exemplo: é uma linguagem de coleta de lixo, mas também possui objetos de valor ("structs" na terminologia local, que são diferentes das estruturas do C ++) que possuem semântica de cópia. O C # evita o problema (1) tendo uma separação clara entre os tipos de referência e valor e (2) sempre copiando os tipos de valor usando a cópia de byte a byte, não o código do usuário, evitando exceções durante a cópia.
Jules
2

Algo que acho que não foi mencionado aqui é que existem eficiências provenientes da coleta de lixo. Nos coletores Java mais usados, o principal local em que os objetos são alocados é uma área reservada para um coletor de cópias. Quando as coisas começam, esse espaço está vazio. À medida que os objetos são criados, eles são alocados um no lado do outro no grande espaço aberto até que ele não possa alocar um no espaço contíguo restante. O GC entra em ação e procura por objetos neste espaço que não estejam mortos. Ele copia os objetos ativos para outra área e os reúne (ou seja, sem fragmentação). O espaço antigo é considerado limpo. Em seguida, ele continua alocando objetos firmemente e repete esse processo, conforme necessário.

Existem dois benefícios nisso. A primeira é que não se gasta tempo excluindo os objetos não utilizados. Depois que os objetos ativos são copiados, a ardósia é considerada limpa e os objetos mortos são simplesmente esquecidos. Em muitas aplicações, a maioria dos objetos não dura muito tempo; portanto, o custo de copiar o conjunto ao vivo é barato comparado às economias obtidas por não ter que se preocupar com o conjunto morto.

O segundo benefício é que, quando um novo objeto é alocado, não há necessidade de procurar uma área contígua. A VM sempre sabe onde o próximo objeto será colocado (ressalva: simplificado ignorando a simultaneidade.)

Esse tipo de coleta e alocação é muito rápido. De uma perspectiva geral da taxa de transferência, é difícil vencer em muitos cenários. O problema é que alguns objetos permanecerão por mais tempo do que você deseja copiá-los e, finalmente, isso significa que o coletor pode precisar fazer uma pausa por um período significativo de vez em quando e quando isso acontecer pode ser imprevisível. Dependendo da duração da pausa e do tipo de aplicativo, isso pode ou não ser um problema. Há pelo menos um coletor sem pausa . Espero que exista uma troca de menor eficiência para obter a natureza sem pausa, mas uma das pessoas que fundou a empresa (Gil Tene) é um super especialista na GC e suas apresentações são uma excelente fonte de informações sobre a GC.

JimmyJames
fonte
O Azul foi fundado por um grupo de GC, pessoal de compilador, pessoal de desempenho e ex-pessoal de CPU Java da Sun. Eles sabem o que eles estão fazendo.
Jörg W Mittag
@ JörgWMittag atualizado para refletir que havia mais de um fundador. graças
JimmyJames
1

Ou é mais como "descartar objetos em C ++ é realmente complicado - passo 20% do meu tempo nele e, no entanto, vazamentos de memória são um lugar comum"?

Na minha experiência pessoal em C ++ e até C, vazamentos de memória nunca foram um grande esforço a evitar. Com o procedimento de teste sensato e o Valgrind, por exemplo, qualquer vazamento físico causado por uma chamada operator new/mallocsem correspondência delete/freeé frequentemente detectado e corrigido rapidamente. Para ser justo, algumas grandes bases de códigos C ++ ou C ++ da velha escola podem ter alguns casos de borda obscuros que podem vazar fisicamente alguns bytes de memória aqui e ali, como resultado de não deleting/freeingnaquele caso de borda que voou sob o radar dos testes.

No entanto, no que diz respeito às observações práticas, os aplicativos mais vazios que encontro (como os que consomem mais e mais memória quanto mais tempo você os executa, mesmo que a quantidade de dados com os quais trabalhamos não esteja aumentando) normalmente não são escritos em C ou C ++. Não encontro coisas como o Linux Kernel ou o Unreal Engine ou mesmo o código nativo usado para implementar o Java na lista de softwares com vazamentos que encontro.

O tipo mais proeminente de software com vazamento que costumo encontrar são coisas como miniaplicativos em Flash, como jogos em Flash, mesmo que eles usem coleta de lixo. E isso não é uma comparação justa se alguém deduzir algo disso, já que muitos aplicativos Flash são escritos por desenvolvedores iniciantes que provavelmente carecem de bons princípios de engenharia e procedimentos de teste (e da mesma forma, tenho certeza de que existem profissionais qualificados trabalhando com a GC que não lute contra software com vazamento), mas eu teria muito a dizer para quem pensa que o GC impede a gravação de software com vazamento.

Ponteiros pendurados

Agora, vindo do meu domínio específico, experiência e como um usando principalmente C e C ++ (e espero que os benefícios do GC variem dependendo de nossas experiências e necessidades), a coisa mais imediata que o GC resolve para mim não são problemas práticos de vazamento de memória, mas oscilando o acesso do ponteiro, e isso poderia literalmente ser um salva-vidas em cenários de missão crítica.

Infelizmente, em muitos casos em que o GC resolve o que seria um acesso de ponteiro danificado, ele substitui o mesmo tipo de erro do programador por um vazamento de memória lógica.

Se você imaginar esse jogo em Flash escrito por um codificador iniciante, ele pode armazenar referências a elementos do jogo em várias estruturas de dados, fazendo com que compartilhem a propriedade desses recursos do jogo. Infelizmente, digamos que ele cometa um erro ao esquecer de remover os elementos do jogo de uma das estruturas de dados ao avançar para o próximo estágio, impedindo que sejam liberados até que o jogo inteiro seja encerrado. No entanto, o jogo ainda parece funcionar bem porque os elementos não estão sendo desenhados ou afetam a interação do usuário. No entanto, o jogo está começando a usar cada vez mais memória, enquanto as taxas de quadros funcionam em uma apresentação de slides, enquanto o processamento oculto ainda circula por essa coleção oculta de elementos no jogo (que agora se tornou explosivo em tamanho). Esse é o tipo de problema que encontro com frequência nesses jogos em Flash.

  • Eu encontrei pessoas dizendo que isso não conta como um "vazamento de memória" porque a memória ainda está sendo liberada após o fechamento do aplicativo e, em vez disso, pode ser chamada de 'vazamento de espaço' ou algo nesse sentido. Embora essa distinção possa ser útil para identificar e falar sobre problemas, não considero essas distinções tão úteis nesse contexto se estivermos falando sobre ela como se não fosse tão problemática quanto um "vazamento de memória" quando estivermos lidando o objetivo prático de garantir que o software não consuma quantidades ridículas de memória quanto mais tempo o executamos (a menos que falemos de sistemas operacionais obscuros que não liberam a memória de um processo quando ele é finalizado).

Agora vamos dizer que o mesmo desenvolvedor iniciante escreveu o jogo em C ++. Nesse caso, normalmente haveria apenas uma estrutura central de dados no jogo que "possui" a memória enquanto outros apontam para essa memória. Se ele cometer o mesmo tipo de erro, é provável que, ao avançar para a próxima etapa, o jogo travar como resultado do acesso a indicadores pendentes (ou pior, fazer algo diferente de travar).

Esse é o tipo mais imediato de troca que eu costumo encontrar no meu domínio com mais frequência entre o GC e o GC. Na verdade, eu não ligo muito para o GC no meu domínio, o que não é muito crítico, porque as maiores lutas que já tive com software com vazamentos envolveram o uso aleatório do GC em uma equipe anterior, causando o tipo de vazamento descrito acima .

No meu domínio particular, na verdade, prefiro o software travando ou danificando-o em muitos casos, porque isso é pelo menos muito mais fácil de detectar do que tentar descobrir por que o software está misteriosamente consumindo quantidades explosivas de memória depois de executá-lo por meia hora enquanto todo o nosso os testes de unidade e integração passam sem nenhuma reclamação (nem mesmo da Valgrind, pois a memória está sendo liberada pelo GC após o desligamento). No entanto, isso não é um slam da GC da minha parte, nem uma tentativa de dizer que é inútil ou algo assim, mas não houve nenhum tipo de bala de prata, nem mesmo de perto, nas equipes com as quais trabalhei contra software com vazamento (para pelo contrário, tive a experiência oposta de que uma base de código utilizando o GC é a mais vazada que já encontrei). Para ser justo, muitos membros dessa equipe nem sabiam o que eram referências fracas,

Propriedade compartilhada e psicologia

O problema que encontro com a coleta de lixo que pode torná-lo tão propenso a "vazamentos de memória" (e insistirei em chamá-lo como tal, como o 'vazamento de espaço' se comporta exatamente da mesma maneira da perspectiva do usuário) nas mãos de aqueles que não o usam com cuidado se relacionam com "tendências humanas" até certo ponto em minha experiência. O problema com essa equipe e a base de código mais vazia que eu já encontrei foi que eles pareciam ter a impressão de que o GC permitiria que parassem de pensar em quem possui recursos.

No nosso caso, tínhamos tantos objetos referenciando um ao outro. Os modelos referenciam os materiais, juntamente com a biblioteca de materiais e o sistema de sombreamento. Os materiais referenciam as texturas junto com a biblioteca de texturas e certos shaders. As câmeras armazenariam referências a todos os tipos de entidades de cena que deveriam ser excluídas da renderização. A lista parecia continuar indefinidamente. Isso fez com que praticamente todos os recursos pesados ​​do sistema pertencessem e estendessem sua vida útil em mais de 10 outros lugares no estado do aplicativo de uma só vez, e isso era muito, muito propenso a erros humanos de um tipo que se traduziriam em vazamentos (e não menor, estou falando de gigabytes em minutos com sérios problemas de usabilidade). Conceitualmente, todos esses recursos não precisavam ser compartilhados na propriedade, todos eles conceitualmente tinham um proprietário,

Se pararmos de pensar em quem possui qual memória e, felizmente, apenas armazenar referências que duram a vida toda a objetos em todo o lugar sem pensar nisso, o software não falhará como resultado de indicadores pendentes, mas quase certamente, sob esse mentalidade descuidada, comece a vazar memória como louca de maneiras muito difíceis de rastrear e que escapam a testes.

Se há um benefício prático para o ponteiro oscilante no meu domínio, é que ele causa falhas e travamentos muito desagradáveis. E isso tende a pelo menos incentivar os desenvolvedores, se eles querem enviar algo confiável, para começar a pensar sobre gerenciamento de recursos e fazer as coisas apropriadas necessárias para remover todas as referências / indicadores adicionais para um objeto que não é mais necessário conceitualmente.

Gerenciamento de recursos de aplicativos

Gerenciamento adequado de recursos é o nome do jogo, se estivermos falando de evitar vazamentos em aplicativos de longa duração, com estado persistente sendo armazenado, onde o vazamento pode causar sérios problemas de taxa de quadros e usabilidade. E gerenciar corretamente os recursos aqui não é menos difícil com ou sem GC. O trabalho não é menos manual para remover as referências apropriadas aos objetos que não são mais necessários, sejam eles ponteiros ou referências que prolongam a vida útil.

Esse é o desafio em meu domínio, não esquecendo o deleteque fazemos new(a menos que falemos horas amadoras com testes, práticas e ferramentas de má qualidade). E isso requer reflexão e cuidado, se estamos usando o GC ou não.

Multithreading

A outra questão que considero muito útil com o GC, se puder ser usada com muito cuidado em meu domínio, é simplificar o gerenciamento de recursos em contextos de multithreading. Se tomarmos cuidado para não armazenar referências que estendem a vida útil a recursos em mais de um local no estado do aplicativo, a natureza de extensão da vida útil das referências de GC pode ser extremamente útil como uma maneira de os segmentos estenderem temporariamente um recurso que está sendo acessado. sua vida útil por apenas uma curta duração, conforme necessário para o encadeamento concluir o processamento.

Eu acho que o uso muito cuidadoso do GC dessa maneira pode produzir um software muito correto, que não está vazando, enquanto simultaneamente simplifica o multithreading.

Existem maneiras de contornar isso, embora o GC esteja ausente. No meu caso, unificamos a representação da entidade da cena do software, com threads que temporariamente fazem com que os recursos da cena sejam estendidos por breves períodos de forma bastante generalizada antes de uma fase de limpeza. Isso pode parecer um pouco com o GC, mas a diferença é que não há "propriedade compartilhada" envolvida, apenas um design de processamento de cena uniforme em encadeamentos que adiam a destruição dos referidos recursos. Mesmo assim, seria muito mais simples confiar apenas no GC aqui se ele pudesse ser usado com muito cuidado com desenvolvedores conscientes, com cuidado para usar referências fracas nas áreas persistentes relevantes, para esses casos de multithreading.

C ++

Finalmente:

Em C ++, tenho que chamar delete para descartar um objeto criado no final de seu ciclo de vida.

No C ++ moderno, isso geralmente não é algo que você deve fazer manualmente. Não se trata tanto de esquecer de fazê-lo. Quando você envolve o tratamento de exceções na imagem, mesmo que você tenha escrito uma deletechamada abaixo para alguma chamada new, algo pode aparecer no meio e nunca alcançá-la deletese você não confiar nas chamadas destruidoras automáticas inseridas pelo compilador para fazer isso por você.

Com o C ++, você praticamente precisa, a menos que esteja trabalhando como um contexto incorporado, com exceções desativadas e bibliotecas especiais programadas deliberadamente para não serem lançadas, evite a limpeza manual de recursos (que inclui evitar chamadas manuais para desbloquear um mutex fora de um dtor , por exemplo, e não apenas desalocação de memória). O tratamento de exceções exige muito, portanto toda a limpeza de recursos deve ser automatizada através de destruidores em sua maior parte.

Dragon Energy
fonte