Por que coleta de lixo se houver ponteiros inteligentes?

62

Hoje em dia, muitos idiomas são coletados como lixo. Está ainda disponível para C ++ por terceiros. Mas o C ++ tem RAII e ponteiros inteligentes. Então, qual é o objetivo de usar a coleta de lixo? Está fazendo algo extra?

E em outras linguagens como C #, se todas as referências forem tratadas como ponteiros inteligentes (mantendo RAII de lado), por especificação e por implementação, ainda haverá alguma necessidade de coletores de lixo? Se não, então por que não é assim?

    
por Gulshan 27.12.2010 / 12:58
fonte

10 respostas

67

So, what's the point of using garbage collection?

Estou supondo que você quer dizer ponteiros inteligentes contados de referência e observarei que eles são uma forma (rudimentar) de coleta de lixo, então vou responder à pergunta "quais são as vantagens de outras formas de coleta de lixo sobre a referência contada ponteiros inteligentes "em vez disso.

  • Precisão . Somente a contagem de referência vaza os ciclos para que os ponteiros inteligentes contados na referência vazem memória em geral, a menos que outras técnicas sejam adicionadas aos ciclos de captura. Uma vez que essas técnicas são adicionadas, o benefício da simplicidade da contagem de referência desapareceu. Além disso, observe que os GCs de contagem e rastreamento de referência baseados em escopo coletam valores em momentos diferentes, às vezes, a contagem de referência coleta antes e às vezes rastreia os GCs coletados anteriormente.

  • Taxa de transferência . Os ponteiros inteligentes são uma das formas menos eficientes de coleta de lixo, especialmente no contexto de aplicativos multithread quando as contagens de referência são colididas atomicamente. Existem técnicas avançadas de contagem de referências projetadas para aliviar isso, mas o rastreamento de GCs ainda é o algoritmo escolhido em ambientes de produção.

  • Latência . Implementações típicas de ponteiros inteligentes permitem que os destruidores avaliem, resultando em tempos de pausa ilimitados. Outras formas de coleta de lixo são muito mais incrementais e podem até ser em tempo real, por ex. Esteira de Baker.

por 27.12.2010 / 15:48
fonte
60

Como ninguém analisou esse ângulo, reformularei sua pergunta: por que colocar algo na linguagem se você puder fazer isso em uma biblioteca? Ignorando detalhes específicos de implementação e sintática, GC / ponteiros inteligentes é basicamente um caso especial dessa questão. Por que definir um coletor de lixo no próprio idioma se você puder implementá-lo em uma biblioteca?

Existem algumas respostas para essa pergunta. O mais importante primeiro:

  1. Você garante que todo o código possa usá-lo para interoperar. Esse é, acredito, o grande motivo pelo qual a reutilização de código e o compartilhamento de código não decolar até Java / C # / Python / Ruby. As bibliotecas precisam se comunicar, e a única linguagem compartilhada confiável que elas têm é o que está na própria especificação da linguagem (e, até certo ponto, sua biblioteca padrão). Se você já tentou reutilizar bibliotecas em C ++, provavelmente experimentou a dor horrenda que nenhuma semântica de memória padrão causa. Eu quero passar uma estrutura para algum lib. Eu passo uma referência? Ponteiro %código%? %código%? Eu estou passando a propriedade ou não? Existe uma maneira de indicar isso? E se a lib precisa alocar? Eu tenho que dar um alocador? Ao não tornar o gerenciamento de memória parte da linguagem, o C ++ força cada par de bibliotecas a negociar sua própria estratégia específica aqui, e é realmente difícil fazer com que todas concordem. O GC faz com que isso não seja um problema completo.

  2. Você pode criar a sintaxe em torno dele. Como o C ++ não encapsula o gerenciamento de memória em si, ele precisa fornecer vários ganchos sintáticos para permitir que o código no nível do usuário expresse todos os detalhes. Você tem ponteiros, referências, scoped_ptr , operadores de referência, operadores indiretos, endereço, etc. Se você distribuir o gerenciamento de memória no próprio idioma, a sintaxe pode ser projetada em torno disso. Todos esses operadores desaparecem e a linguagem fica mais limpa e simples.

  3. Você obtém um alto retorno do investimento. O valor gerado por qualquer parte do código é multiplicado pelo número de pessoas que o usam. Isso significa que quanto mais usuários você tem, mais você pode gastar em um software. Quando você move um recurso para o idioma, todos os usuários do idioma o usarão. Isso significa que você pode alocar mais esforço do que poderia para uma biblioteca usada apenas por um subconjunto desses usuários. É por isso que linguagens como Java e C # possuem VMs de primeira classe e coletores de lixo fantasticamente de alta qualidade: o custo de desenvolvê-las é amortizado em milhões de usuários.

por 27.12.2010 / 23:06
fonte
29

Coleta de lixo basicamente significa que seus objetos alocados são liberados automaticamente quando não estão mais sendo referenciados.

Mais precisamente, eles são liberados quando se tornam inacessíveis para o programa, já que objetos referenciados circularmente nunca seriam liberados de outra forma.

Os ponteiros inteligentes referem-se apenas a qualquer estrutura que se comporta como um ponteiro comum, mas tem alguma funcionalidade extra anexada. Esses incluem , mas não estão limitados a desalocação, mas também a cópias-em-gravação, verificações vinculadas, ...

Agora, como você afirmou, os ponteiros inteligentes podem ser usados para implementar uma forma de coleta de lixo.

Mas a linha de pensamento segue o seguinte caminho:

  1. A coleta de lixo é algo bacana de se ter, pois é conveniente e eu tenho que cuidar de menos coisas
  2. Portanto: eu quero a coleta de lixo na minha linguagem
  3. Agora, como posso obter o GC na minha língua?

É claro que você pode projetar assim desde o início. C # foi projetado para ser coletado como lixo, então apenas new do seu objeto e ele será liberado quando as referências estiverem fora do escopo. Como isso é feito, depende do compilador.

Mas em C ++, não havia coleta de lixo. Se alocarmos algum ponteiro int* p = new int; e ele ficar fora do escopo, p será removido da pilha, mas ninguém cuidará da memória alocada.

Agora, a única coisa que você tem desde o começo são destruidores deterministas . Quando um objeto deixa o escopo em que foi criado, seu destruidor é chamado. Em combinação com modelos e sobrecarga de operadores, você pode criar um objeto wrapper que se comporte como um ponteiro, mas use a funcionalidade de destruição para limpar os recursos anexados a ele (RAII). Você chama isso de ponteiro inteligente .

Tudo isto é altamente específico para C ++: sobrecarga do operador, modelos, destruidores, ... Nesta situação em particular, você desenvolveu ponteiros inteligentes para fornecer o GC que você deseja.

Mas se você projetar uma linguagem com o GC desde o início, isso é apenas um detalhe de implementação. Você acabou de dizer que o objeto será limpo e o compilador fará isso por você.

Ponteiros inteligentes como em C ++ provavelmente não seriam possíveis em linguagens como as do C #, que não têm nenhuma destruição determinística (o C # trabalha em torno disso, fornecendo um açúcar sintático para chamar um .Dispose() em certos objetos). Recursos não referenciados serão finalmente reivindicados pelo GC, mas serão indefinidos quando exatamente isso acontecer.

E isso, por sua vez, pode permitir que o GC faça seu trabalho de forma mais eficiente. Sendo construído mais profundamente na linguagem do que os ponteiros inteligentes, que são definidos sobre ele, o .NET GC pode, por exemplo, atrasar operações de memória e executá-las em blocos para torná-las mais baratas ou até mesmo mover a memória para aumentar a eficiência com base na frequência com que os objetos são acessados.

    
por 27.12.2010 / 13:24
fonte
4

Existem duas grandes diferenças, em minha opinião, entre a coleta de lixo e os ponteiros inteligentes usados para o gerenciamento de memória:

  1. Os ponteiros inteligentes não podem coletar lixo cíclico; coleta de lixo pode
  2. Os ponteiros inteligentes fazem todo o trabalho nos momentos de referência, desreferência e desalocação, no encadeamento do aplicativo; coleta de lixo não precisa

O primeiro significa que o GC coletará lixo que os ponteiros inteligentes não irão; Se você estiver usando ponteiros inteligentes, evite criar esse tipo de lixo ou esteja preparado para lidar com isso manualmente.

O último significa que não importa o quão inteligentes são os ponteiros inteligentes, sua operação irá desacelerar os threads de trabalho em seu programa. A coleta de lixo pode adiar o trabalho e movê-lo para outros segmentos; que permite ser mais eficiente em geral (na verdade, o custo de tempo de execução de um GC moderno é menor do que um sistema malloc / free normal, mesmo sem a sobrecarga extra de ponteiros inteligentes) e fazer o trabalho que ainda precisa fazer sem entrar no maneira dos threads de aplicação.

Agora, observe que os ponteiros inteligentes, sendo construções programáticas, podem ser usados para fazer todo tipo de outras coisas interessantes - veja a resposta de Dario - que estão completamente fora do escopo da coleta de lixo. Se você quiser fazer isso, precisará de ponteiros inteligentes.

No entanto, para fins de gerenciamento de memória, não vejo qualquer perspectiva de indicadores inteligentes substituindo a coleta de lixo. Eles simplesmente não são tão bons nisso.

    
por 27.12.2010 / 13:51
fonte
3

O termo coleta de lixo implica que há algum lixo para coletar. Em C ++, os ponteiros inteligentes vêm em vários tipos, o mais importante é o unique_ptr. O unique_ptr é basicamente uma propriedade única de escopo e propriedade. Em um trecho de código bem projetado, a maioria dos itens alocados em heap normalmente residiria atrás de unique_ptr smart pointer e a propriedade desses recursos será bem definida em todos os momentos. Não há praticamente nenhuma sobrecarga em unique_ptr e unique_ptr elimina a maioria dos problemas de gerenciamento de memória que tradicionalmente direcionavam as pessoas para idiomas gerenciados. Agora que mais núcleos em execução concomitantemente estão se tornando mais comuns, os princípios de design que orientam o código a usar uma propriedade única e bem definida em qualquer momento tornam-se mais importantes para o desempenho. O uso do modelo de computação do ator permite a construção de programas com uma quantidade mínima de estado compartilhado entre encadeamentos, e a propriedade exclusiva desempenha um papel importante em tornar os sistemas de alto desempenho um uso eficiente de muitos núcleos sem a sobrecarga de compartilhamento entre encadeamentos de dados e os requisitos de mutex implícitos.

Mesmo em um programa bem projetado, especialmente em ambientes multi-encadeados, nem tudo pode ser expresso sem estruturas de dados compartilhadas, e para aquelas estruturas de dados que realmente requerem, os encadeamentos precisam se comunicar. RAII em c ++ funciona muito bem para preocupações de vida em uma única configuração de thread, em uma configuração multi threaded a vida útil dos objetos pode não ser completamente pilha hierarquicamente definida. Para essas situações, o uso de shared_ptr oferece uma grande parte da solução. Você cria propriedade compartilhada de um recurso e isso em C ++ é o único lugar em que vemos lixo, mas em quantidades tão pequenas que um programa c ++ projetado adequadamente deve ser considerado mais para implementar coleta de lixo com ptr compartilhado do que coleta de lixo completa como implementado em outros idiomas. C ++ simplesmente não tem muito 'lixo' para coletar.

Como afirmam outros, os ponteiros inteligentes de referência são uma forma de coleta de lixo, e um para isso tem um grande problema. O exemplo que é usado principalmente como desvantagem de formulários contados de referência da coleta de lixo é o problema com a criação de estruturas de dados órfãs conectadas a ponteiros inteligentes entre si que criam clusters de objetos que impedem que os outros sejam coletados. Enquanto em um programa projetado de acordo com o modelo de computação do ator, as estruturas de dados normalmente não permitem clusters não colecionáveis que surjam em C ++, quando você usa a abordagem de dados compartilhados para programação multi-threaded, como é usado predominantemente em grande parte da indústria, esses clusters órfãos podem se tornar rapidamente uma realidade.

Então, para somar tudo, se por uso de ponteiro compartilhado você quer dizer o uso amplo de unique_ptr combinado com o modelo de ator da abordagem de computação para programação multi-thread e o uso limitado de shared_ptr, que outras formas de coleta de lixo não compre-lhe quaisquer benefícios adicionais. Se, no entanto, uma abordagem de tudo compartilhado permitir que você acabe com shared_ptr em todo o lugar, convém mudar modelos de simultaneidade ou mudar para uma linguagem gerenciada mais voltada para o compartilhamento mais amplo de propriedade e acesso simultâneo a estruturas de dados.

    
por 26.01.2016 / 11:29
fonte
2

A maioria dos indicadores inteligentes é implementada usando a contagem de referência. Ou seja, cada ponteiro inteligente que se refere a um objeto incrementa a contagem de referência de objetos. Quando essa contagem chega a zero, o objeto é liberado.

O problema existe se você tiver referências circulares. Isto é, A tem uma referência a B, B tem uma referência a C e C tem uma referência a A. Se você estiver usando ponteiros inteligentes, então para liberar a memória associada com A, B & C você precisa entrar manualmente um "quebrar" a referência circular (por exemplo, usando weak_ptr em C + +).

A coleta de lixo (normalmente) funciona de maneira bem diferente. A maioria dos coletores de lixo atualmente usa um teste de alcance . Isto é, ele observa todas as referências na pilha e aquelas que são globalmente acessíveis e, em seguida, rastreia todos os objetos aos quais essas referências se referem, e objetos que eles se referem, etc. Todo o resto é lixo .

Dessa forma, referências circulares não importam mais - desde que nem A, B e C sejam alcançáveis , a memória pode ser recuperada.

Existem outras vantagens para a coleta de lixo "real". Por exemplo, a alocação de memória é extremamente barata: basta incrementar o ponteiro para o "fim" do bloco de memória. A desalocação tem um custo amortizado constante também. Mas é claro que linguagens como o C ++ permitem que você implemente o gerenciamento de memória da maneira que desejar, para que você possa criar uma estratégia de alocação ainda mais rápida.

É claro que, em C ++, a quantidade de memória alocada por heap é tipicamente menor que uma linguagem pesada como C # / .NET. Mas isso não é realmente uma questão de coleta de lixo versus ponteiros inteligentes.

De qualquer forma, o problema não é um pouco melhor do que o outro. Cada um deles tem vantagens e desvantagens.

    
por 27.12.2010 / 13:45
fonte
2

É sobre o desempenho . Desalocar memória requer muita administração. Se a desalocação for executada em segundo plano, o desempenho do processo de primeiro plano aumenta. Infelizmente, a alocação de memória não pode ser preguiçosa (os objetos alocados serão usados no próximo momento sagrado), mas liberar objetos pode.

Tente em C ++ (sem qualquer GC) para alocar um grande grupo de objetos, imprima "hello" e exclua-os. Você ficará surpreso com quanto tempo leva para liberar objetos.

Além disso, o GNU libc fornece ferramentas mais eficazes para desalocar a memória, consulte obstacks . Tenho que notar, não tenho experiência com obstáculos, nunca os usei.

    
por 27.12.2010 / 15:11
fonte
2

A coleta de lixo pode ser mais eficiente - basicamente "sobrecarrega" a sobrecarga do gerenciamento de memória e faz tudo de uma vez. Em geral, isso resultará em menos CPU total sendo gasta na desalocação de memória, mas isso significa que você terá uma grande explosão de atividade de desalocação em algum momento. Se o GC não for projetado adequadamente, isso pode se tornar visível para o usuário como uma 'pausa' enquanto o CG tenta desfazer a alocação de memória. A maioria dos GCs modernos é muito boa em manter isso invisível para o usuário, exceto sob as condições mais adversas.

Os ponteiros inteligentes (ou qualquer esquema de contagem de referência) têm a vantagem de acontecer exatamente quando você espera ver o código (o ponteiro inteligente fica fora do escopo, a coisa é excluída). Você tem pequenas explosões de desalocação aqui e ali. No geral, você pode usar mais tempo de CPU na desalocação, mas como está distribuído em todas as coisas que acontecem no programa, é menos provável (desparafinar a estrutura de dados de um monstro) ficar visível para o usuário.

Se você está fazendo algo em que a capacidade de resposta é importante, sugiro que os indicadores inteligentes / contagem de referência informem exatamente quando as coisas estão acontecendo, para que você possa saber enquanto codifica o que provavelmente ficará visível para seus usuários. Em uma configuração de GC, você tem apenas o mais efêmero controle sobre o coletor de lixo e simplesmente tem que tentar contornar a coisa.

Por outro lado, se a taxa de transferência geral é o seu objetivo, um sistema baseado em GC pode ser uma escolha muito melhor, pois minimiza os recursos necessários para o gerenciamento de memória.

Ciclos: não considero o problema dos ciclos significativo. Em um sistema onde você tem ponteiros inteligentes, você tende a estruturas de dados que não têm ciclos, ou você é simplesmente cuidadoso sobre como deixar essas coisas de lado. Se necessário, os objetos do detentor que sabem quebrar os ciclos nos objetos de propriedade podem ser usados para garantir automaticamente a destruição adequada. Em alguns domínios da programação isso pode ser importante, mas para a maioria dos trabalhos do dia a dia, é irrelevante.

    
por 27.12.2010 / 18:09
fonte
1

A limitação número um de ponteiros inteligentes é que eles nem sempre ajudam contra referências circulares. Por exemplo, você tem o objeto A armazenando um ponteiro inteligente para o objeto B e o objeto B está armazenando um ponteiro inteligente para o objeto A. Se eles forem deixados juntos sem redefinir nenhum dos ponteiros, eles nunca serão desalocados.

Isso acontece porque um ponteiro inteligente precisa executar uma ação específica que não será triigerada no cenário acima porque ambos os objetos são inacessíveis ao programa. A coleta de lixo funcionará - ela identificará corretamente que os objetos não podem ser atingidos no programa e serão coletados.

    
por 27.12.2010 / 13:40
fonte
0

Por favor, lembre-se que, no final, tudo se resume a uma CPU executando instruções. Que eu saiba, todas as CPUs de classe de consumidor têm conjuntos de instruções que exigem que você tenha dados armazenados em um determinado local na memória e você tem ponteiros para os dados mencionados. Isso é tudo o que você tem no nível básico.

Tudo sobre isso com coleta de lixo, referências a dados que podem ter sido movidos, compactação de heap, etc. etc. está fazendo o trabalho dentro das restrições dadas pelo paradigma "fragmento de memória com um ponteiro de endereço" acima. Mesma coisa com ponteiros inteligentes - você ainda tem que fazer o código rodar em hardware real.

    
por 27.12.2010 / 14:13
fonte