Pergunta

Continuo ouvindo pessoas reclamando que C++ não tem coleta de lixo.Também ouvi dizer que o Comitê de Padrões C++ está pensando em adicioná-lo à linguagem.Receio não ver o sentido disso...usar RAII com ponteiros inteligentes elimina a necessidade disso, certo?

Minha única experiência com coleta de lixo foi em alguns computadores domésticos baratos dos anos 80, onde isso significava que o sistema travava por alguns segundos de vez em quando.Tenho certeza de que melhorou desde então, mas como você pode imaginar, isso não me deixou com uma opinião positiva sobre isso.

Que vantagens a coleta de lixo poderia oferecer a um desenvolvedor C++ experiente?

Foi útil?

Solução

Eu continuo ouvindo pessoas reclamando que o C ++ não tem coleta de lixo.

Sinto muito por eles. Seriamente.

O C ++ tem RAII, e eu sempre reclamo não encontrar Raii (ou um RAII castrado) em idiomas coletados de lixo.

Que vantagens a coleta de lixo poderia oferecer um desenvolvedor de C ++ experiente?

Outra ferramenta.

Matt J escreveu bem em seu post (Coleta de lixo em C ++ - Por quê?): Não precisamos de recursos de C ++, pois a maioria deles pode ser codificada em C, e não precisamos de recursos C, pois a maioria deles pode codificar na montagem, etc. C ++ deve evoluir.

Como desenvolvedor: não me importo com o GC. Eu tentei RAII e GC, e acho Raii muito superior. Como dito por Greg Rogers em seu post (Coleta de lixo em C ++ - Por quê?), os vazamentos de memória não são tão terríveis (pelo menos em C ++, onde são raros se o C ++ for realmente usado) que justifique o GC em vez de RAII. GC tem desalocação/finalização não determinística e é apenas uma maneira de Escreva um código que simplesmente não se importe com opções de memória específicas.

Esta última frase é importante: é importante escrever código que "Juste não se importa". Da mesma maneira em C ++ Raii, não nos importamos com a libertação de Ressource porque Raii faz isso por nós, ou para a inicialização do objeto porque o construtor faz isso por nós, às vezes é importante codificar sem se preocupar com quem é o proprietário de qual memória, E que ponteiro tipo (compartilhado, fraco, etc.) precisamos para este ou este código de código. Parece haver uma necessidade de GC em C ++. (Mesmo se eu personalmente não vê)

Um exemplo de bom uso do GC em C ++

Às vezes, em um aplicativo, você tem "dados flutuantes". Imagine uma estrutura de dados em forma de árvore, mas ninguém é realmente "proprietário" dos dados (e ninguém realmente se importa quando exatamente será destruído). Vários objetos podem usá -lo e, em seguida, descartá -lo. Você quer que seja libertado quando ninguém mais o está usando.

A abordagem C ++ está usando um ponteiro inteligente. O Boost :: shared_ptr vem à mente. Portanto, cada peça de dados é de propriedade de seu próprio ponteiro compartilhado. Legal. O problema é que, quando cada um dos dados pode se referir a outro dados. Você não pode usar ponteiros compartilhados porque eles estão usando um contador de referência, que não suportará referências circulares (A PONTS para B e B aponta para A). Portanto, você deve saber muito sobre onde usar ponteiros fracos (Boost :: frAct_ptr) e quando usar ponteiros compartilhados.

Com um GC, você apenas usa os dados estruturados da árvore.

A desvantagem é que você não deve se importar quando Os "dados flutuantes" serão realmente destruídos. Só isso vai ser destruído.

Conclusão

Então, no final, se feito corretamente e compatível com os idiomas atuais de C ++, GC seria um Mais uma boa ferramenta para C ++.

C ++ é um idioma multiparadigma: adicionar um GC talvez faça alguns fanboys de C ++ chorarem por causa da traição, mas no final, pode ser uma boa ideia, e acho que os padrões C ++ comite não deixar Linguagem, para que possamos confiar neles para fazer o trabalho necessário para ativar um C ++ GC correto que não interfira no C ++: Como sempre no C ++, se você não precisar de um recurso, não o use e isso não custará nada.

Outras dicas

A resposta curta é que a coleção de lixo é muito semelhante em princípio ao RAII com indicadores inteligentes. Se cada peça de memória que você alocar mentir dentro de um objeto, e esse objeto é referido apenas por ponteiros inteligentes, você terá algo próximo à coleta de lixo (potencialmente melhor). A vantagem vem de não ter que ser tão criteriosa sobre o escopo e a pontuação inteligente de todos os objetos e deixar o tempo de execução fazer o trabalho para você.

Esta pergunta parece análoga a "O que o C ++ tem para oferecer ao desenvolvedor de montagem experiente? Instruções e sub -rotinas eliminam a necessidade, certo?"

Com o advento de bons verificadores de memória como o valgrind, não vejo muita utilidade na coleta de lixo como uma rede de segurança "no caso" de esquecermos de desalocar algo - especialmente porque isso não ajuda muito no gerenciamento do caso mais genérico de recursos além da memória (embora sejam muito menos comuns).Além disso, alocar e desalocar explicitamente memória (mesmo com ponteiros inteligentes) é bastante raro no código que vi, já que os contêineres geralmente são uma maneira muito mais simples e melhor.

Mas a coleta de lixo pode oferecer potencialmente benefícios de desempenho, especialmente se muitos objetos de vida curta estiverem sendo alocados no heap.O GC também oferece potencialmente melhor localidade de referência para objetos recém-criados (comparável aos objetos na pilha).

O fator motivador para o suporte ao GC no C ++ parece ser a programação do Lambda, funções anônimas etc. Acontece que as bibliotecas Lambda se beneficiam da capacidade de alocar memória sem se preocupar com a limpeza. O benefício para os desenvolvedores comuns seria mais simples, confiável e mais rápido, compilando as bibliotecas Lambda.

O GC também ajuda a simular a memória infinita; A única razão pela qual você precisa excluir pods é que você precisa reciclar a memória. Se você possui uma memória GC ou Infinite, não há mais necessidade de excluir pods.

O comitê não está adicionando coleta de lixo, eles estão adicionando alguns recursos que permitem que a coleção de lixo seja implementada com mais segurança. Somente o tempo dirá se eles realmente têm algum efeito nos futuros compiladores. As implementações específicas podem variar amplamente, mas provavelmente envolverão a coleta baseada em acessibilidade, o que pode envolver um leve jarro, dependendo de como é feito.

Uma coisa é que, porém, nenhum coletor de lixo conforme os padrões poderá chamar os destruidores - apenas para reutilizar silenciosamente a memória perdida.

Que vantagens a coleta de lixo poderia oferecer um desenvolvedor de C ++ experiente?

Não precisar perseguir vazamentos de recursos no código dos colegas menos experientes.

Não entendo como se pode argumentar que o RAII substitui o GC ou é muito superior.Existem muitos casos tratados por um gc que o RAII simplesmente não consegue resolver.São feras diferentes.

Primeiro, o RAII não é à prova de balas:funciona contra algumas falhas comuns que são generalizadas em C++, mas há muitos casos em que RAII não ajuda em nada;é frágil a eventos assíncronos (como sinais no UNIX).Fundamentalmente, o RAII depende do escopo:quando uma variável está fora do escopo, ela é automaticamente liberada (assumindo que o destruidor esteja implementado corretamente, é claro).

Aqui está um exemplo simples onde nem auto_ptr nem RAII podem ajudá-lo:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <memory>

using namespace std;

volatile sig_atomic_t got_sigint = 0;

class A {
        public:
                A() { printf("ctor\n"); };
                ~A() { printf("dtor\n"); };
};

void catch_sigint (int sig)
{
        got_sigint = 1;
}

/* Emulate expensive computation */
void do_something()
{
        sleep(3);
}

void handle_sigint()
{
        printf("Caught SIGINT\n");
        exit(EXIT_FAILURE);
}

int main (void)
{
        A a;
        auto_ptr<A> aa(new A);

        signal(SIGINT, catch_sigint);

        while (1) {
                if (got_sigint == 0) {
                        do_something();
                } else {
                        handle_sigint();
                        return -1;
                }
        }
}

O destruidor de A nunca será chamado.É claro que se trata de um exemplo artificial e um tanto inventado, mas uma situação semelhante pode realmente acontecer;por exemplo, quando seu código é chamado por outro código que lida com SIGINT e sobre o qual você não tem controle algum (exemplo concreto:extensões mex em matlab).É a mesma razão pela qual finalmente em python não garante a execução de algo.Gc pode ajudá-lo neste caso.

Outras expressões não funcionam bem com isso:em qualquer programa não trivial, você precisará de objetos com estado (estou usando a palavra objeto em um sentido muito amplo aqui, pode ser qualquer construção permitida pela linguagem);se você precisar controlar o estado fora de uma função, não poderá fazer isso facilmente com o RAII (é por isso que o RAII não é tão útil para programação assíncrona).OTOH, gc tem uma visão de toda a memória do seu processo, ou seja, conhece todos os objetos que alocou e pode limpar de forma assíncrona.

Também pode ser muito mais rápido usar gc, pelos mesmos motivos:se você precisar alocar/desalocar muitos objetos (em particular objetos pequenos), o gc superará amplamente o RAII, a menos que você escreva um alocador personalizado, já que o gc pode alocar/limpar muitos objetos em uma passagem.Alguns projetos C++ bem conhecidos usam gc, mesmo quando o desempenho é importante (veja, por exemplo, Tim Sweenie sobre o uso de gc no Unreal Tournament: http://lambda-the-ultimate.org/node/1277).O GC basicamente aumenta o rendimento às custas da latência.

Claro, há casos em que RAII é melhor que gc;em particular, o conceito gc preocupa-se principalmente com a memória, e esse não é o único recurso.Coisas como arquivo, etc ...pode ser bem tratado com RAII.Linguagens sem manipulação de memória como python ou ruby ​​​​têm algo como RAII para esses casos, BTW (com instrução em python).RAII é muito útil quando você precisa controlar precisamente quando o recurso é liberado, e isso geralmente acontece com arquivos ou bloqueios, por exemplo.

É um erro totalmente comum assumir que porque o C ++ não tem coleta de lixo assado no idioma, você não pode usar a coleta de lixo no período C ++. Isso não faz sentido. Conheço os programadores de elite C ++ que usam o coletor da Boehm como uma questão de seu trabalho.

A coleta de lixo permite adiar a decisão sobre quem possui um objeto.

O C ++ usa a semântica do valor; portanto, com o RAII, de fato, os objetos são lembrados ao sair do escopo. Às vezes, isso é chamado de "GC imediato".

Quando o seu programa começa a usar a semantics de referência (através de indicadores inteligentes etc ...), o idioma não suporta mais você, você é deixado para a inteligência da sua biblioteca de ponteiro inteligente.

O complicado sobre o GC é decidir sobre quando um objeto não é mais necessário.

A coleta de lixo faz RCU Sincronização sem trava muito mais fácil de implementar de maneira correta e eficiente.

Segurança e escalabilidade mais fáceis de threads

Há uma propriedade do GC que pode ser muito importante em alguns cenários. A atribuição de ponteiro é naturalmente atômica na maioria das plataformas, enquanto a criação de ponteiros de referência segura para roscas ("Smart") é bastante difícil e introduz a sobrecarga significativa da sincronização. Como resultado, indicadores inteligentes são frequentemente informados de "não escalar bem" na arquitetura de vários núcleos.

A coleta de lixo é realmente a base para o gerenciamento automático de recursos. E ter o GC muda a maneira como você resolve os problemas de uma maneira difícil de quantificar. Por exemplo, quando você está fazendo gerenciamento manual de recursos, você precisa:

  • Considere quando um item pode ser libertado (todos os módulos/classes terminaram com ele?)
  • Considere quem é a responsabilidade é libertar um recurso quando estiver pronto para ser libertado (qual classe/módulo deve libertar este item?)

No caso trivial, não há complexidade. Por exemplo, você abre um arquivo no início de um método e o fecha no final. Ou o chamador deve libertar este bloco de memória retornado.

As coisas começam a ficar complicadas rapidamente quando você tem vários módulos que interagem com um recurso e não está tão claro quem precisa limpar. O resultado final é que toda a abordagem para resolver um problema inclui certos padrões de programação e design que são um compromisso.

Em idiomas com coleta de lixo, você pode usar um descartável Padrão onde você pode liberar recursos que você sabe que terminou, mas se você não conseguir libertar o GC para salvar o dia.


Ponteiros inteligentes, na verdade, um exemplo perfeito dos compromissos que mencionei. Ponteiros inteligentes não podem salvá -lo de vazar estruturas de dados cíclicos, a menos que você tenha um mecanismo de backup. Para evitar esse problema, você geralmente se compromete e evita usar uma estrutura cíclica, mesmo que possa ser o melhor ajuste.

Eu também tenho dúvidas de que o C ++ Commitee está adicionando uma coleção de lixo completa ao padrão.

Mas eu diria que o principal motivo para adicionar/ter coleta de lixo na linguagem moderna é que existem poucos bons motivos contra coleta de lixo. Desde os anos 80, houve vários avanços enormes no campo de gerenciamento de memória e coleta de lixo e acredito que existem até estratégias de coleta de lixo que podem dar a você garantias de tempo suave (como "GC não levará mais do que .. .. no pior caso ").

usar RAII com ponteiros inteligentes elimina a necessidade disso, certo?

Ponteiros inteligentes podem ser usados ​​para implementar a contagem de referências em C++, que é uma forma de coleta de lixo (gerenciamento automático de memória), mas os GCs de produção não usam mais a contagem de referências porque ela apresenta algumas deficiências importantes:

  1. Ciclos de contagem de vazamentos de referência.Considere A↔B, ambos os objetos A e B referem-se um ao outro, então ambos têm uma contagem de referência de 1 e nenhum deles é coletado, mas ambos devem ser recuperados.Algoritmos avançados como exclusão de teste resolver este problema, mas adicionar muita complexidade.Usando weak_ptr como solução alternativa, recorremos ao gerenciamento manual de memória.

  2. A contagem de referência ingênua é lenta por vários motivos.Em primeiro lugar, exige que as contagens de referência fora do cache sejam alteradas com frequência (consulte O shared_ptr do Boost é até 10× mais lento que a coleta de lixo do OCaml).Em segundo lugar, os destruidores injetados no final do escopo podem incorrer em chamadas de funções virtuais desnecessárias e caras e inibir otimizações, como a eliminação de chamadas finais.

  3. A contagem de referência baseada no escopo mantém o lixo flutuando, pois os objetos não são reciclados até o final do escopo, enquanto os GCs de rastreamento podem recuperá-los assim que se tornarem inacessíveis, por exemplo.um local alocado antes de um loop pode ser recuperado durante o loop?

Que vantagens a coleta de lixo poderia oferecer a um desenvolvedor C++ experiente?

Produtividade e confiabilidade são os principais benefícios.Para muitas aplicações, o gerenciamento manual de memória exige um esforço significativo do programador.Ao simular uma máquina de memória infinita, a coleta de lixo libera o programador dessa carga, o que permite que ele se concentre na resolução de problemas e evite algumas classes importantes de bugs (ponteiros pendentes, falta de free, dobro free).Além disso, a coleta de lixo facilita outras formas de programação, por ex.resolvendo o problema funarg para cima (1970).

Em uma estrutura que suporta o GC, uma referência a um objeto imutável, como uma string, pode ser transmitido da mesma maneira que um primitivo. Considere a classe (C# ou Java):

public class MaximumItemFinder
{
  String maxItemName = "";
  int maxItemValue = -2147483647 - 1;

  public void AddAnother(int itemValue, String itemName)
  {
    if (itemValue >= maxItemValue)
    {
      maxItemValue = itemValue;
      maxItemName = itemName;
    }
  }
  public String getMaxItemName() { return maxItemName; }
  public int getMaxItemValue() { return maxItemValue; }
}

Observe que este código nunca tem a ver com o conteúdo de qualquer uma das cordas e pode simplesmente tratá -las como primitivas. Uma declaração como maxItemName = itemName; Provavelmente gerará duas instruções: uma carga de registro seguida por uma loja de registros. o MaximumItemFinder não terá como saber se os chamadores de AddAnother vão manter qualquer referência às cordas passadas, e os chamadores não terão como saber quanto tempo MaximumItemFinder manterá referências a eles. Chamadores de getMaxItemName não terá como saber se e quando MaximumItemFinder e o fornecedor original da string devolvido abandonou todas as referências a ele. Porque o código pode simplesmente passar referências de string como valores primitivos, no entanto, Nenhuma dessas coisas importa.

Observe também que, embora a classe acima não seria segura na presença de chamadas simultâneas para AddAnother, qualquer chamada para GetMaxItemName seria garantido devolver uma referência válida a uma corda vazia ou a uma das cordas que foram passadas para AddAnother. A sincronização de threads seria necessária se alguém quisesse garantir qualquer relação entre o nome do item máximo e seu valor, mas A segurança da memória é garantida mesmo em sua ausência.

Eu não acho que exista alguma maneira de escrever um método como o acima no C ++, o que defenderia a segurança da memória na presença de uso arbitrário de vários threads sem usar a sincronização de threads ou exigir que cada variável de string tenha sua própria cópia de seu conteúdo , mantido em seu próprio espaço de armazenamento, que não pode ser liberado ou realocado durante a vida útil da variável em questão. Certamente não seria possível definir um tipo de referência de cordas que pudesse ser definido, atribuído e transmitido tão barato quanto um int.

A coleção de lixo pode fazer vazamentos o seu pior pesadelo

GC completo que lida com coisas como referências cíclicas seriam um pouco de atualização em relação a um refinado shared_ptr. Eu o receberia um pouco em C ++, mas não no nível do idioma.

Uma das belezas sobre o C ++ é que ele não force a coleta de lixo em você.

Eu quero corrigir um equívoco comum: uma coleção de lixo mito que de alguma forma elimina vazamentos. Pela minha experiência, os piores pesadelos do código de depuração escritos por outras pessoas e tentando identificar os vazamentos lógicos mais caros envolveram a coleção de lixo com idiomas como o Python incorporado por meio de um aplicativo de host que estiver intensivo em recursos.

Ao falar sobre assuntos como o GC, há teoria e depois há prática. Em teoria, é maravilhoso e evita vazamentos. No entanto, no nível teórico, também é maravilhoso e livre de vazamentos, pois, em teoria, todos escrevem código perfeitamente corretos e testavam todos os casos possíveis em que uma única peça de código poderia dar errado.

A coleta de lixo combinada com a colaboração de equipes menos do que ideal causou os piores vazamentos e mais difíceis de fugir do nosso caso.

O problema ainda tem a ver com a propriedade de recursos. Você precisa tomar decisões claras de design aqui quando objetos persistentes estão envolvidos, e a coleta de lixo facilita demais pensar que você não.

Dado algum recurso, R, em um ambiente de equipe em que os desenvolvedores não estão constantemente se comunicando e revisando o código uns dos outros cuidadosamente no Alll Times (algo um pouco comum demais na minha experiência), torna -se muito fácil para o desenvolvedor A Para armazenar um identificador nesse recurso. Desenvolvedor B também, talvez de uma maneira obscura que indiretamente acrescente R para alguma estrutura de dados. O mesmo acontece C. Em um sistema coletado de lixo, isso criou 3 proprietários de R.

Porque desenvolvedor A foi aquele que criou o recurso originalmente e acha que ele é o proprietário dele, ele se lembra de liberar a referência a R Quando o usuário indica que ele não quer mais usá -lo. Afinal, se ele não o fizer, nada aconteceria e seria óbvio ao testar que a lógica de remoção de extremidade do usuário não fez nada. Então, ele se lembra de lançá -lo, como qualquer desenvolvedor razoavelmente competente faria. Isso desencadeia um evento para o qual B lida com isso e também lembra para liberar a referência a R.

No entanto, C esquece. Ele não é um dos desenvolvedores mais fortes da equipe: um recruta um tanto novo que só trabalha no sistema há um ano. Ou talvez ele nem esteja na equipe, apenas um popular desenvolvedor de terceiros que escrevem plugins para nosso produto que muitos usuários adicionam ao software. Com a coleta de lixo, é quando obtemos esses vazamentos silenciosos de recursos lógicos. Eles são o pior tipo: eles não se manifestam necessariamente no lado visível do usuário do software como um bug óbvio, além do fato de que, por durações da execução do programa, o uso da memória continua a subir e aumentar para algum propósito misterioso. Tentar restringir esses problemas com um depurador pode ser tão divertido quanto depurar uma condição de corrida sensível ao tempo.

Sem coleta de lixo, desenvolvedor C teria criado um ponteiro pendurado. Ele pode tentar acessá -lo em algum momento e fazer com que o software falhe. Agora esse é um bug de teste/usuário. C Fica um pouco envergonhado e corrige seu bug. No cenário do GC, apenas tentar descobrir onde o sistema está vazando pode ser tão difícil que alguns dos vazamentos nunca são corrigidos. Estes não são valgrind-Vazamentos físicos do tipo que podem ser detectados facilmente e identificados para uma linha específica de código.

Com coleta de lixo, desenvolvedor C criou um vazamento muito misterioso. Seu código pode continuar a acessar R O que agora é apenas uma entidade invisível no software, irrelevante para o usuário neste momento, mas ainda em um estado válido. E como CO código cria mais vazamentos, ele está criando um processamento mais oculto em recursos irrelevantes, e o software não está apenas vazando memória, mas também ficando mais lento a cada vez.

Portanto, a coleta de lixo não mitiga necessariamente vazamentos de recursos lógicos. Em cenários menos do que ideais, pode facilitar muito os vazamentos de passar silenciosamente despercebidos e permanecer no software. Os desenvolvedores podem ficar tão frustrados tentando rastrear seus vazamentos lógicos do GC que simplesmente dizem aos usuários para reiniciar o software periodicamente como uma solução alternativa. Ele elimina os indicadores pendentes e, em um software obcecado por segurança, onde o travamento é completamente inaceitável em qualquer cenário, eu preferiria o GC. Mas muitas vezes estou trabalhando em produtos críticos de desempenho menos críticos, mas intensivos em recursos, em que um acidente que pode ser corrigido prontamente é preferível a um bug silencioso realmente obscuro e misterioso, e os vazamentos de recursos não são insetos triviais lá.

Nos dois casos, estamos falando de objetos persistentes que não residem na pilha, como um gráfico de cenas em um software 3D ou os videoclipes disponíveis em um compositor ou inimigos em um mundo de jogos. Quando os recursos vinculam sua vida útil à pilha, o C ++ e qualquer outro idioma GC tendem a tornar triviais gerenciar os recursos corretamente. A verdadeira dificuldade está em recursos persistentes que referenciam outros recursos.

Em C ou C ++, você pode ter ponteiros e falhas pendentes resultantes de Segfaults se você não designar claramente quem possui um recurso e quando lidar com eles deve ser lançado (ex: definido como nulo em resposta a um evento). No entanto, no GC, esse acidente alto e desagradável, mas muitas vezes, é trocado por um vazamento silencioso de recursos que nunca pode ser detectado.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top