Por que os ponteiros inteligentes de contagem de referência são tão populares?

52

Como posso ver, ponteiros inteligentes são amplamente utilizados em muitos projetos C ++ do mundo real.

Embora algum tipo de ponteiro inteligente seja obviamente benéfico para oferecer suporte a transferências de propriedade e RAII, também há uma tendência de usar ponteiros compartilhados por padrão , como uma forma de "coleta de lixo" , para que o programador não precise pensar muito na alocação. .

Por que os ponteiros compartilhados são mais populares do que integrar um coletor de lixo adequado como o Boehm GC ? (Ou você concorda que eles são mais populares que os GCs reais?)

Conheço duas vantagens dos GCs convencionais sobre a contagem de referência:

  • Os algoritmos convencionais de GC não têm problemas com ciclos de referência .
  • A contagem de referência é geralmente mais lenta que um GC adequado.

Quais são as razões para usar ponteiros inteligentes de contagem de referência?

Miklós Homolya
fonte
6
Eu apenas adicionaria um comentário que esse é um padrão errado para usar: na maioria dos casos, std::unique_ptré suficiente e, como tal, tem zero de sobrecarga em relação aos ponteiros brutos em termos de desempenho em tempo de execução. Ao usar em std::shared_ptrqualquer lugar, você também obscureceria a semântica da propriedade, perdendo um dos principais benefícios de indicadores inteligentes que não sejam o gerenciamento automático de recursos - a compreensão clara da intenção por trás do código.
Matt
2
Desculpe, mas a resposta aceita aqui está completamente errada. A contagem de referência tem custos indiretos mais altos (uma contagem em vez de um bit de marca e desempenho mais lento em tempo de execução), tempos de pausa ilimitados quando diminui a avalanche e não é mais complexo que, por exemplo, o semi-espaço de Cheney.
precisa saber é o seguinte

Respostas:

57

Algumas vantagens da contagem de referência sobre a coleta de lixo:

  1. Baixa sobrecarga. Os coletores de lixo podem ser bastante intrusivos (por exemplo, congelar o programa em momentos imprevisíveis enquanto um ciclo de coleta de lixo é processado) e consumir muita memória (por exemplo, a pegada de memória do processo aumenta desnecessariamente para muitos megabytes antes que a coleta de lixo finalmente entre em ação)

  2. Comportamento mais previsível. Com a contagem de referências, você garante que seu objeto será liberado no instante em que a última referência desaparecer. Com a coleta de lixo, por outro lado, seu objeto será liberado "algum dia", quando o sistema chegar a ele. Para a RAM, isso geralmente não é um grande problema em desktops ou servidores com pouca carga, mas para outros recursos (por exemplo, identificadores de arquivos), você geralmente precisa que eles sejam fechados o mais rápido possível para evitar possíveis conflitos mais tarde.

  3. Mais simples. A contagem de referência pode ser explicada em alguns minutos e implementada em uma ou duas horas. Os coletores de lixo, especialmente aqueles com desempenho decente, são extremamente complexos e poucas pessoas os entendem.

  4. Padrão. O C ++ inclui contagem de referência (via shared_ptr) e amigos no STL, o que significa que a maioria dos programadores de C ++ está familiarizado com ele e a maior parte do código C ++ funciona com ele. Porém, não há nenhum coletor de lixo C ++ padrão, o que significa que você deve escolher um e esperar que funcione bem para o seu caso de uso - e, se não funcionar, é problema seu corrigir, não o idioma.

Quanto às supostas desvantagens da contagem de referência - não detectar ciclos é um problema, mas que eu nunca encontrei pessoalmente nos últimos dez anos usando a contagem de referência. A maioria das estruturas de dados é naturalmente acíclica e, se você se deparar com uma situação em que precisa de referências cíclicas (por exemplo, ponteiro pai em um nó de árvore), poderá usar apenas um fraco_ptr ou um ponteiro C bruto para a "direção inversa". Desde que você esteja ciente do possível problema ao projetar suas estruturas de dados, isso não é problema.

Quanto ao desempenho, nunca tive um problema com o desempenho da contagem de referência. Eu tive problemas com o desempenho da coleta de lixo, em particular os congelamentos aleatórios em que o GC pode incorrer, para os quais a única solução ("não alocar objetos") também pode ser reformulada como "não use o GC" .

Jeremy Friesner
fonte
16
As implementações ingênuas de contagem de referência normalmente obtêm um rendimento muito menor do que os GCs de produção (30 a 40%) à custa da latência. A lacuna pode ser preenchida com otimizações, como usar menos bits para a refcount e evitar o rastreamento de objetos até que eles escapem - o C ++ faz isso durar naturalmente se você principalmente make_sharedretornar. Ainda assim, a latência tende a ser o maior problema em aplicativos em tempo real, mas o rendimento é mais importante em geral, e é por isso que o rastreamento de GCs é tão amplamente usado. Eu não seria tão rápido em falar mal deles.
precisa saber é o seguinte
3
Eu questionaria 'mais simples': mais simples em termos da quantidade total de implementação necessária , sim, mas não mais simples para o código que o utiliza : compare dizendo a alguém como usar o RC ('faça isso ao criar objetos e isso ao destruí-los' ) de como (ingenuamente, o que é suficiente) usar o GC ('...').
precisa saber é o seguinte
4
"Com a contagem de referência, você garante que seu objeto será liberado no instante em que a última referência a ele desaparecer". Esse é um equívoco comum. flyingfrogblog.blogspot.co.uk/2013/10/...
Jon Harrop
4
@ JonHarrop: Esse post no blog é horrivelmente errado. Você também deve ler todos os comentários, especialmente o último.
Deduplicator
3
@ JonHarrop: Sim, existe. Ele não entende que a vida é o escopo completo que vai até a chave de fechamento. E a otimização em F # que, de acordo com os comentários, às vezes funciona apenas, está encerrando a vida útil mais cedo, se a variável não for usada novamente. Que naturalmente tem seus próprios perigos.
Deduplicator
26

Para obter um bom desempenho de um GC, ele precisa ser capaz de mover objetos na memória. Em uma linguagem como C ++, na qual você pode interagir diretamente com os locais da memória, isso é praticamente impossível. (O Microsoft C ++ / CLR não conta porque introduz uma nova sintaxe para os ponteiros gerenciados pelo GC e, portanto, é efetivamente uma linguagem diferente.)

O Boehm GC, embora seja uma idéia bacana, é o pior dos dois mundos: você precisa de um malloc () mais lento que um bom GC e, portanto, perde o comportamento determinístico de alocação / desalocação sem o aumento de desempenho correspondente de um GC geracional . Além disso, é necessariamente conservador, por isso não necessariamente coletará todo o seu lixo.

Um GC bom e bem ajustado pode ser uma grande coisa. Mas em uma linguagem como C ++, os ganhos são mínimos e os custos geralmente não valem a pena.

Será interessante ver, no entanto, à medida que o C ++ 11 se torna mais popular, se as lambdas e a semântica de captura começam a levar a comunidade C ++ para os mesmos tipos de problemas de alocação e vida útil do objeto que fizeram com que a comunidade Lisp inventasse GCs no primeiro Lugar, colocar.

Veja também minha resposta para uma pergunta relacionada no StackOverflow .

Daniel Pryden
fonte
6
RE, o Boehm GC, eu ocasionalmente me perguntei o quanto ele é pessoalmente responsável pela aversão tradicional ao GC entre programadores C e C ++, simplesmente fornecendo uma má primeira impressão da tecnologia em geral.
Leushenko
@ Leushenko Bem dito. Um caso em questão é essa questão, em que Boehm gc é chamado de gc "adequado", ignorando o fato de ser lento e praticamente garantido que vazará. Encontrei essa pergunta ao pesquisar se alguém implementou o disjuntor em estilo python para shared_ptr, o que parece ser uma meta válida para uma implementação em c ++.
user4815162342
4

Como posso ver, ponteiros inteligentes são amplamente utilizados em muitos projetos C ++ do mundo real.

Verdadeiro, mas, objetivamente, a grande maioria do código agora é escrita em linguagens modernas com rastreadores de coletores de lixo.

Embora algum tipo de ponteiro inteligente seja obviamente benéfico para oferecer suporte a transferências de propriedade e RAII, também há uma tendência de usar ponteiros compartilhados por padrão, como uma forma de "coleta de lixo", para que o programador não precise pensar muito na alocação. .

Essa é uma péssima idéia, porque você ainda precisa se preocupar com os ciclos.

Por que os ponteiros compartilhados são mais populares do que integrar um coletor de lixo adequado como o Boehm GC? (Ou você concorda que eles são mais populares que os GCs reais?)

Oh uau, há muitas coisas erradas na sua linha de pensamento:

  1. O GC de Boehm não é um GC "adequado" em nenhum sentido da palavra. É realmente horrível. É conservador, portanto vaza e é ineficiente por design. Consulte: http://flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. Os ponteiros compartilhados são, objetivamente, nem de longe tão populares quanto o GC, porque a grande maioria dos desenvolvedores está usando os idiomas do GC agora e não precisa de ponteiros compartilhados. Basta olhar para Java e Javascript no mercado de trabalho em comparação com C ++.

  3. Você parece estar restringindo a consideração ao C ++ porque, presumo, você acha que o GC é uma questão tangencial. Não é (a única maneira de obter um GC decente é projetar o idioma e a VM para ele desde o início), então você está introduzindo o viés de seleção. As pessoas que realmente querem uma coleta de lixo adequada não ficam com o C ++.

Quais são as razões para usar ponteiros inteligentes de contagem de referência?

Você está restrito ao C ++, mas gostaria de ter um gerenciamento automático de memória.

Jon Harrop
fonte
7
Hum, é uma pergunta marcada com c ++ que fala sobre recursos de C ++. Claramente, qualquer declaração geral é mencionada no código C ++, não na totalidade da programação. Portanto, no entanto, a coleta de lixo "objetivamente" pode estar em uso fora do mundo C ++, o que é irrelevante para a questão em questão.
Nicol Bolas
2
Sua última linha está evidentemente errada: você está em C ++ e está feliz por não ser obrigado a lidar com o GC e isso atrasa a liberação de recursos. Há um motivo pelo qual a Apple não gosta do GC, e a diretriz mais importante para os idiomas do GC é: Não crie lixo a menos que você tenha muitos recursos ociosos ou não possa ajudá-lo.
Deduplicator
3
@ JonHarrop: Então, compare pequenos programas equivalentes com e sem GC, que não são explicitamente escolhidos para serem usados ​​em benefício de ambos os lados. Qual deles você espera que precise de mais memória?
Deduplicator
11
@ Reduplicador: posso imaginar programas que gerem resultados. A contagem de referência superaria o rastreamento do GC quando o programa for projetado para manter a pilha alocar memória até que ele sobreviva ao berçário (por exemplo, uma fila de listas), porque esse é o desempenho patológico de um GC geracional e geraria o lixo mais flutuante. O rastreamento da coleta de lixo exigiria menos memória do que a contagem de referência baseada em escopo, quando há muitos objetos pequenos e a vida útil é curta, mas não é conhecida estaticamente; portanto, algo como um programa lógico usando estruturas de dados puramente funcionais.
precisa saber é o seguinte
3
@ JonHarrop: Eu quis dizer com GC (rastreamento ou o que seja) e RAII se você fala C ++. O que inclui contagem de referência, mas apenas onde é útil. Ou você pode comparar com um programa Swift.
Deduplicator
3

No MacOS X e iOS, e com os desenvolvedores que usam Objective-C ou Swift, a contagem de referências é popular porque é tratada automaticamente, e o uso da coleta de lixo diminuiu consideravelmente, já que a Apple não suporta mais (soube que aplicativos usando a coleta de lixo será interrompida na próxima versão do MacOS X e a coleta de lixo nunca foi implementada no iOS). Na verdade, duvido seriamente que houvesse muito software usando a coleta de lixo quando estava disponível.

O motivo para se livrar da coleta de lixo: nunca funcionou de maneira confiável em um ambiente de estilo C, onde os ponteiros poderiam "escapar" para áreas não acessíveis pelo coletor de lixo. A Apple acredita e acredita que a contagem de referências é mais rápida. (Você pode fazer reivindicações aqui sobre velocidade relativa, mas ninguém foi capaz de convencer a Apple). E no final, ninguém usou a coleta de lixo.

A primeira coisa que qualquer desenvolvedor de MacOS X ou iOS aprende é como lidar com ciclos de referência, para que não seja um problema para um desenvolvedor real.

gnasher729
fonte
Pelo que entendi, não era um ambiente do tipo C que decidia as coisas, mas o GC é indeterminista e precisa de muito mais memória para ter um desempenho aceitável, e fora do servidor / área de trabalho sempre um pouco escasso.
Deduplicator
Depuração porque o coletor de lixo destruiu um objeto que eu ainda estava usando (levando a um acidente) decidiu que para mim :-)
gnasher729
Ah, sim, isso também faria. Você acabou descobrindo o porquê?
Deduplicator
Sim, foi uma das muitas funções do Unix em que você passa um vazio * como um "contexto" que é devolvido a você em uma função de retorno de chamada; o void * era realmente um objeto Objective-C, e o coletor de lixo não sabia que o objeto estava escondido na chamada do Unix. O retorno de chamada é chamado, lança o void * para o Object *, kaboom!
precisa saber é o seguinte
2

A maior desvantagem da coleta de lixo no C ++ é que é absolutamente impossível acertar:

  • No C ++, os ponteiros não vivem em sua própria comunidade murada, eles são misturados com outros dados. Como tal, não é possível distinguir um ponteiro de outros dados que, por acaso, possuem um padrão de bits que pode ser interpretado como um ponteiro válido.

    Conseqüência: Qualquer coletor de lixo C ++ vazará objetos que devem ser coletados.

  • No C ++, você pode fazer aritmética de ponteiros para derivar ponteiros. Assim, se você não encontrar um ponteiro para o início de um bloco, isso não significa que esse bloco não possa ser referenciado.

    Conseqüência: Qualquer coletor de lixo C ++ precisa levar em consideração esses ajustes, tratando qualquer sequência de bits que aponte para qualquer lugar dentro de um bloco, inclusive logo após o término, como um ponteiro válido que faça referência ao bloco.

    Nota: Nenhum coletor de lixo C ++ pode manipular código com truques como estes:

    int* array = new int[7];
    array--;    //undefined behavior, but people may be tempted anyway...
    for(int i = 1; i <= 7; i++) array[i] = i;

    É verdade que isso invoca um comportamento indefinido. Mas algum código existente é mais inteligente do que bom, e pode desencadear uma desalocação preliminar por um coletor de lixo.

cmaster
fonte
2
" eles são misturados com outros dados. " Não é tanto que eles são "misturados" com outros dados. É fácil usar o sistema do tipo C ++ para ver o que é um ponteiro e o que não é. O problema é que os ponteiros frequentemente se tornam outros dados. Ocultar um ponteiro em um número inteiro é infelizmente uma ferramenta comum para muitas APIs do estilo C.
Nicol Bolas
11
Você nem precisa de um comportamento indefinido para estragar um coletor de lixo em c ++. Você pode, por exemplo, serializar um ponteiro para um arquivo e lê-lo posteriormente. Enquanto isso, seu processo pode não conter esse ponteiro em nenhum lugar do espaço de endereço, para que o coletor de lixo possa coletar esse objeto e, quando você desserializar o ponteiro ... Ops.
precisa saber é
@Bwmat "Even"? Escrever ponteiros para um arquivo como esse parece um pouco ... exagerado. De qualquer forma, o mesmo problema sério atormenta os ponteiros para empilhar objetos; eles podem desaparecer quando você lê o ponteiro do arquivo em outro lugar do código! Desserializar o valor inválido do ponteiro é um comportamento indefinido, não faça isso.
hyde 27/01
Se for claro, você precisaria ter cuidado se estiver fazendo algo assim. Era para ser um exemplo de que, em geral, um coletor de lixo não pode trabalhar 'corretamente' em todos os casos em C ++ (sem alterar o idioma)
Bwmat
11
@ gnasher729: Ehm, não? Ponteiros de fim de passado estão perfeitamente bem?
Deduplicator