Atualmente, muitos idiomas são coletados como lixo. Está disponível até para C ++ por terceiros. Mas o C ++ possui RAII e ponteiros inteligentes. Então, qual é o sentido de usar a coleta de lixo? Está fazendo algo extra?
E em outros idiomas como C #, se todas as referências forem tratadas como ponteiros inteligentes (mantendo o RAII de lado), por especificação e por implementação, ainda haverá a necessidade de coletores de lixo? Se não, então por que não é assim?
garbage-collection
smart-pointer
Gulshan
fonte
fonte
Respostas:
Suponho que você queira dizer ponteiros inteligentes contados por referência e observarei que eles são uma forma (rudimentar) de coleta de lixo, então responderei à pergunta "quais são as vantagens de outras formas de coleta de lixo em relação a ponteiros inteligentes contados" em vez de.
Precisão . Somente a contagem de referência vaza ciclos, de modo que os ponteiros inteligentes contados por referência vazam memória em geral, a menos que outras técnicas sejam adicionadas para capturar ciclos. Depois 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 com base no escopo coletam valores em momentos diferentes; às vezes, a contagem de referência coleta mais cedo e, às vezes, os GCs de rastreamento coletam mais cedo.
Rendimento . Ponteiros inteligentes são uma das formas menos eficientes de coleta de lixo, particularmente no contexto de aplicativos multithread quando as contagens de referência são aumentadas atomicamente. Existem técnicas avançadas de contagem de referência projetadas para aliviar isso, mas o rastreamento de GCs ainda é o algoritmo de escolha nos ambientes de produção.
Latência . As 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 exemplo, a esteira de Baker.
fonte
Como ninguém o examinou desse ângulo, reformularemos sua pergunta: por que colocar algo no idioma se você pode fazer isso em uma biblioteca? Ignorando a implementação específica e os detalhes sintáticos, o GC / ponteiros inteligentes é basicamente um caso especial dessa questão. Por que definir um coletor de lixo no próprio idioma, se você pode implementá-lo em uma biblioteca?
Existem algumas respostas para essa pergunta. O mais importante primeiro:
Você garante que todo o código possa usá-lo para interoperar. Acho que esse é o grande motivo pelo qual a reutilização e o compartilhamento de código não decolaram até o Java / C # / Python / Ruby. As bibliotecas precisam se comunicar, e a única linguagem compartilhada confiável que eles têm é o que está na própria especificação de linguagem (e, até certo ponto, em 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 alguma lib. Eu passo uma referência? Ponteiro?
scoped_ptr
?smart_ptr
? Estou passando propriedade, ou não? Existe uma maneira de indicar isso? E se a biblioteca precisar 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 todos concordem. O GC torna isso um problema não completo.Você pode criar a sintaxe ao seu redor. Como o C ++ não encapsula o gerenciamento de memória, ele precisa fornecer uma série de ganchos sintáticos para permitir que o código no nível do usuário expresse todos os detalhes. Você tem ponteiros, referências,
const
operadores de desreferenciamento, operadores de indireção, endereço de etc. Se você colocar o gerenciamento de memória no próprio idioma, a sintaxe poderá ser projetada em torno disso. Todos esses operadores desaparecem e o idioma fica mais limpo e simples.Você obtém um alto retorno do investimento. O valor que qualquer parte do código gera é multiplicado pelo número de pessoas que o utilizam. Isso significa que, quanto mais usuários você tiver, mais poderá 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 uma biblioteca usada apenas por um subconjunto desses usuários. É por isso que linguagens como Java e C # têm VMs absolutamente de primeira qualidade e coletores de lixo de alta qualidade: o custo de desenvolvê-las é amortizado por milhões de usuários.
fonte
Dispose
um objeto que encapsule um bitmap, qualquer referência a esse objeto será uma referência a um objeto bitmap descartado. Se o objeto foi excluído prematuramente enquanto outro código ainda espera usá-lo, a classe de bitmap pode garantir que o outro código falhe de maneira previsível . Por outro lado, o uso de uma referência à memória liberada é Comportamento indefinido.A coleta de lixo basicamente significa que seus objetos alocados são liberados automaticamente em algum momento depois que não são mais acessíveis.
Mais precisamente, eles são liberados quando se tornam inacessíveis para o programa, pois objetos referenciados circularmente nunca seriam liberados.
Ponteiros inteligentes apenas se referem a qualquer estrutura que se comporte como um ponteiro comum, mas com alguma funcionalidade extra anexada. Eles incluem, entre outros, desalocação, mas também cópia em gravação, cheques encadernados, ...
Agora, como você afirmou, ponteiros inteligentes podem ser usados para implementar uma forma de coleta de lixo.
Mas a linha de pensamento segue da seguinte maneira:
Claro, você pode projetá-lo assim desde o início. O C # foi projetado para ser coletado como lixo, portanto, apenas
new
seu objeto 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 pretendida. Se alocarmos algum ponteiro
int* p = new int;
e ele ficar fora do escopo,p
ele próprio será removido da pilha, mas ninguém cuidará da memória alocada.Agora, a única coisa que você tem desde o início são destruidores determinísticos . Quando um objeto sai do escopo em que foi criado, seu destruidor é chamado. Em combinação com modelos e sobrecarga de operador, você pode projetar um objeto wrapper que se comporte como um ponteiro, mas usa a funcionalidade destruidora para limpar os recursos anexados a ele (RAII). Você chama este de ponteiro inteligente .
Tudo isso é altamente específico em C ++: Sobrecarga de operador, modelos, destruidores, ... Nesta situação específica de linguagem, você desenvolveu indicadores inteligentes para fornecer o GC desejado.
Mas se você projetar uma linguagem com o GC desde o início, isso é apenas um detalhe da 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 C #, que não têm nenhuma destruição determinística (o C # contorna isso, fornecendo açúcar sintático para chamar
.Dispose()
certos objetos). Os recursos não referenciados serão finalmente recuperados pelo GC, mas indefinidos quando exatamente isso acontecerá.E isso, por sua vez, pode permitir que o GC faça seu trabalho com mais eficiência. Sendo incorporado mais profundamente à linguagem do que os ponteiros inteligentes, os quais são definidos por cima, o .NET GC pode, por exemplo, atrasar as operações de memória e executá-las em blocos para torná-las mais baratas ou até mover a memória para aumentar a eficiência com base na frequência com que os objetos são acessados.
fonte
IDisposable
eusing
. Mas requer um pouco de esforço do programador, e é por isso que geralmente é usado apenas para recursos muito escassos, como identificadores de conexão com o banco de dados.IDisposable
sintaxe por apenas substituindo convencionallet ident = value
poruse ident = value
...using
nada tem a ver com coleta de lixo, apenas chama uma função quando uma variável fica fora do escopo, assim como destruidores em C ++.Há duas grandes diferenças, na minha opinião, entre coleta de lixo e ponteiros inteligentes, usados no gerenciamento de memória:
O primeiro significa que o GC coletará lixo que os ponteiros inteligentes não coletarão; se você estiver usando ponteiros inteligentes, evite criar esse tipo de lixo ou esteja preparado para lidar com ele manualmente.
O último significa que, não importa quão inteligentes sejam os ponteiros inteligentes, sua operação diminuirá a velocidade dos threads de trabalho no seu programa. A coleta de lixo pode adiar o trabalho e movê-lo para outros threads; isso permite que seja mais eficiente em geral (na verdade, o custo de tempo de execução de um GC moderno é menor que um sistema malloc / livre normal, mesmo sem a sobrecarga extra de indicadores inteligentes) e faz o trabalho que ainda precisa ser feito sem entrar no maneira dos encadeamentos do aplicativo.
Agora, observe que indicadores inteligentes, como 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 indicadores inteligentes.
No entanto, para fins de gerenciamento de memória, não vejo nenhuma perspectiva de ponteiros inteligentes substituindo a coleta de lixo. Eles simplesmente não são tão bons nisso.
fonte
using
bloco nas versões subseqüentes do C #. Além disso, o comportamento não determinístico dos GCs pode ser proibido em sistemas em tempo real (e é por isso que os GCs não são usados lá). Além disso, não vamos esquecer que os GCs são tão complexos para acertar que na verdade vazam memória e são bastante ineficientes (por exemplo, Boehm ...).O termo coleta de lixo implica que existe algum lixo a ser coletado. No C ++, os ponteiros inteligentes têm vários sabores, principalmente o unique_ptr. O unique_ptr é basicamente uma única propriedade e construção de escopo. Em um pedaço de código bem projetado, a maioria das coisas alocadas em heap normalmente residiria atrás de ponteiros inteligentes unique_ptr e a propriedade desses recursos será bem definida o tempo todo. Não há praticamente nenhuma sobrecarga no unique_ptr e o unique_ptr elimina a maioria dos problemas manuais de gerenciamento de memória que tradicionalmente levavam as pessoas a idiomas gerenciados. Agora que mais núcleos em execução simultânea estão se tornando cada vez mais comuns, os princípios de design que levam o código a usar propriedade exclusiva e bem definida a qualquer momento se tornam mais importantes para o desempenho.
Mesmo em um programa bem projetado, especialmente em ambientes com vários threads, nem tudo pode ser expresso sem estruturas de dados compartilhadas e, para aquelas estruturas de dados que realmente exigem, os threads precisam se comunicar. O RAII no c ++ funciona muito bem para preocupações com a vida útil em uma única instalação de encadeamento; em uma instalação com vários encadeamentos, o tempo de vida dos objetos pode não ser completamente definido hierarquicamente. Para essas situações, o uso do shared_ptr oferece grande parte da solução. Você cria propriedade compartilhada de um recurso e este em C ++ é o único lugar que vemos lixo, mas em quantidades tão pequenas que um programa c ++ projetado adequado deve ser considerado mais para implementar a coleção 'lixo' com ptrs compartilhados do que a coleta de lixo completa como implementado em outros idiomas. C ++ simplesmente não tem tanto 'lixo'
Como declarado por outros, os ponteiros inteligentes contados por referência são uma forma de coleta de lixo, e um deles tem um grande problema. O exemplo usado principalmente como desvantagem das formas de coleta de lixo contadas como referência é o problema com a criação de estruturas de dados órfãs conectadas com ponteiros inteligentes entre si, que criam clusters de objetos que impedem a coleta um do outro. Enquanto em um programa projetado de acordo com o modelo de ator de computação, as estruturas de dados geralmente não permitem que esses clusters não colecionáveis surjam em C ++, quando você usa a abordagem de dados compartilhados para programação multiencadeada, como é usado predominantemente em grande parte da indústria, esses clusters órfãos podem rapidamente se tornar realidade.
Portanto, para resumir tudo, se por uso compartilhado de ponteiro você quer dizer o amplo uso de unique_ptr combinado com o modelo de ator de abordagem computacional para programação multiencadeada e o uso limitado de shared_ptr, que outras formas de coleta de lixo não compram benefícios adicionais. Se, no entanto, uma abordagem de tudo compartilhado permitir que você compartilhe o shared_ptr em todo o lugar, considere mudar modelos de simultaneidade ou mudar para um idioma gerenciado que seja mais voltado para o compartilhamento mais amplo de propriedade e acesso simultâneo às estruturas de dados.
fonte
Rust
que não precisa de coleta de lixo?A maioria dos ponteiros inteligentes é implementada usando a contagem de referência. Ou seja, cada ponteiro inteligente que se refere a um objeto aumenta a contagem de referência dos objetos. Quando essa contagem chega a zero, o objeto é liberado.
O problema existe se você tiver referências circulares. Ou seja, 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, para liberar a memória associada a A, B e C, você precisará manualmente entre lá e "quebra" a referência circular (por exemplo, usando
weak_ptr
em C ++).A coleta de lixo (normalmente) funciona de maneira bem diferente. Atualmente, a maioria dos coletores de lixo usa um teste de acessibilidade . Ou seja, ele analisa todas as referências na pilha e aquelas que são acessíveis globalmente e, em seguida, rastreia todos os objetos aos quais essas referências se referem e objetos a que se referem, etc. Todo o resto é lixo.
Dessa forma, as referências circulares não importam mais - desde que A, B e C não sejam alcançáveis , a memória poderá ser recuperada.
Existem outras vantagens na coleta de lixo "real". Por exemplo, a alocação de memória é extremamente barata: basta incrementar o ponteiro para o "final" do bloco de memória. A desalocação também possui um custo amortizado constante. Mas é claro que linguagens como C ++ permitem implementar o gerenciamento de memória da maneira que você desejar, para que você possa criar uma estratégia de alocação ainda mais rápida.
Obviamente, em C ++, a quantidade de memória alocada ao heap geralmente é menor que uma linguagem pesada como referência, como C # / .NET. Mas isso não é realmente uma questão de coleta de lixo x ponteiros inteligentes.
De qualquer forma, o problema não é fácil; um é melhor que o outro. Cada um deles tem vantagens e desvantagens.
fonte
É sobre desempenho . A desalocação de memória requer muita administração. Se a desalocação for executada em segundo plano, o desempenho do processo em primeiro plano aumentará. 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 a liberação de objetos pode.
Tente em C ++ (sem qualquer GC) para alocar um monte de objetos, imprima "olá" e exclua-os. Você ficará surpreso quanto tempo leva para liberar objetos.
Além disso, o GNU libc fornece ferramentas mais eficazes para desalocar memória, veja obstáculos . Devo notar, não tenho experiência com obstáculos, nunca os usei.
fonte
A coleta de lixo pode ser mais eficiente - basicamente "agrupa" a sobrecarga do gerenciamento de memória e faz tudo de uma vez. Em geral, isso resultará em menos CPU geral sendo gasta na desalocação de memória, mas significa que você terá uma grande explosão de atividade de desalocação em algum momento. Se o GC não for projetado corretamente, isso poderá se tornar visível para o usuário como uma "pausa" enquanto o GC tenta desalocar a memória. A maioria dos GCs modernos é muito boa em manter isso invisível para o usuário, exceto nas condições mais adversas.
Ponteiros inteligentes (ou qualquer esquema de contagem de referência) têm a vantagem de acontecer exatamente quando você esperaria olhar o código (o ponteiro inteligente fica fora do escopo, algo é excluído). Você recebe pequenas explosões de desalocação aqui e ali. No geral, você pode usar mais tempo de CPU na desalocação, mas como está espalhado por todas as coisas que acontecem no seu programa, é menos provável (menos a desalocação de alguma estrutura de dados de monstros) se tornar visível para o usuário.
Se você estiver fazendo algo em que a capacidade de resposta é importante, sugiro que a contagem inteligente de indicadores / referências informe exatamente quando as coisas estão acontecendo, para que você possa saber ao codificar o que provavelmente ficará visível para os usuários. Em uma configuração de GC, você tem apenas o controle mais efêmero sobre o coletor de lixo e simplesmente precisa tentar solucionar o problema.
Por outro lado, se o objetivo geral da taxa de transferência geral, 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 significativo o problema dos ciclos. Em um sistema em que você tem indicadores inteligentes, você tende a estruturas de dados que não têm ciclos ou é simplesmente cuidadoso com a maneira como desiste dessas coisas. Se necessário, os objetos detentores que sabem como interromper 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.
fonte
A limitação número um de indicadores inteligentes é que eles nem sempre ajudam contra referências circulares. Por exemplo, você tem o objeto A armazenando um ponteiro inteligente no objeto B e o objeto B está armazenando um ponteiro inteligente no objeto A. Se eles forem deixados juntos sem redefinir um 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 os dois objetos são inacessíveis ao programa. A coleta de lixo irá lidar - ela identificará adequadamente que os objetos não podem ser acessados pelo programa e serão coletados.
fonte
É um espectro .
Se você não tem limites estreitos no desempenho e está preparado para se esforçar, você terminará na montagem ou c, com todo o ônus de tomar as decisões certas e toda a liberdade para fazer isso, mas com ele , toda a liberdade de estragar tudo:
"Vou te dizer o que fazer, faça. Confie em mim".
A coleta de lixo é o outro extremo do espectro. Você tem muito pouco controle, mas está resolvido para você:
"Vou te dizer o que eu quero, você faz acontecer".
Isso tem muitas vantagens, principalmente porque você não precisa ser tão confiável quando se trata de saber exatamente quando um recurso não é mais necessário, mas (apesar de algumas das respostas estarem por aqui) não é bom para o desempenho, e a previsibilidade do desempenho. (Como todas as coisas, se você tiver controle e fazer algo estúpido, poderá obter resultados piores. No entanto, sugerir que, em tempo de compilação, saber quais são as condições para liberar memória, não pode ser usado como uma vitória no desempenho. além de ingênuo).
RAII, escopo, contagem de referências, etc , são todos auxiliares para permitir que você avance mais ao longo desse espectro, mas não é todo o caminho até lá. Todas essas coisas ainda requerem uso ativo. Eles ainda permitem e exigem que você interaja com o gerenciamento de memória de uma maneira que a coleta de lixo não.
fonte
Lembre-se de que, no final, tudo se resume a uma CPU executando instruções. Que eu saiba, todas as CPUs de nível consumidor possuem conjuntos de instruções que exigem que você tenha dados armazenados em um determinado local na memória e que possui ponteiros para esses dados. Isso é tudo o que você tem no nível básico.
Tudo além disso, 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 "bloco de memória com ponteiro de endereço" acima. A mesma coisa com os ponteiros inteligentes - você AINDA precisa executar o código no hardware real.
fonte