Pourquoi les pointeurs intelligents de comptage de références sont-ils si populaires?

52

Comme je peux le constater, les pointeurs intelligents sont largement utilisés dans de nombreux projets C ++ réels.

Bien que certains pointeurs intelligents soient évidemment bénéfiques pour prendre en charge la RAII et les transferts de propriété, il existe également une tendance à utiliser des pointeurs partagés par défaut , comme moyen de "ramasser les ordures" , afin que le programmeur n'ait pas à réfléchir autant à l'allocation. .

Pourquoi les pointeurs partagés sont-ils plus populaires que l'intégration d'un bon ramasse-ordures comme Boehm GC ? (Ou êtes-vous d'accord pour dire qu'ils sont plus populaires que les GC actuels?)

Je connais deux avantages des CPG classiques par rapport au comptage de références:

  • Les algorithmes GC conventionnels ne posent aucun problème avec les cycles de référence .
  • Le nombre de références est généralement plus lent qu'un GC correct.

Quelles sont les raisons d'utiliser des pointeurs intelligents de comptage de références?

Miklós Homolya
la source
6
J'ajouterais simplement un commentaire indiquant qu'il s'agit d'un paramètre par défaut incorrect: dans la plupart des cas, il std::unique_ptrest suffisant et, en tant que tel, ne génère aucun point brut sur les pointeurs bruts en termes de performances d'exécution. En utilisant std::shared_ptrpartout, vous masqueriez également la sémantique de la propriété, perdant ainsi l'un des principaux avantages des pointeurs intelligents autres que la gestion automatique des ressources: une compréhension claire de l'intention du code.
Matt
2
Désolé, mais la réponse acceptée ici est complètement fausse. Le comptage des références entraîne des frais généraux plus élevés (un compteur au lieu d'un bit de repère et des performances d'exécution plus lentes), des temps de pause illimités lorsque diminue l'avalanche et pas plus complexe que, par exemple, le semi-espace de Cheney.
Jon Harrop

Réponses:

57

Quelques avantages du comptage de références par rapport au ramassage des ordures:

  1. Frais généraux bas. Les éboueurs peuvent être assez intrusifs (par exemple, geler votre programme à des moments imprévisibles pendant qu'un cycle de récupération des déchets est en cours) et gourmands en mémoire (par exemple, l'empreinte mémoire de votre processus augmente inutilement de plusieurs mégaoctets avant que la récupération des déchets ne démarre enfin)

  2. Comportement plus prévisible. Avec le comptage de références, vous avez la garantie que votre objet sera libéré à l’instant où sa dernière référence disparaîtra. Avec la récupération de place, par contre, votre objet sera libéré "parfois", lorsque le système le traitera. Pour la RAM, cela n’est généralement pas un gros problème sur les ordinateurs de bureau ou les serveurs peu chargés, mais pour d’autres ressources (par exemple, les descripteurs de fichiers), il est souvent nécessaire de les fermer au plus vite afin d’éviter des conflits éventuels ultérieurement.

  3. Plus simple. Le comptage des références peut être expliqué en quelques minutes et mis en œuvre en une heure ou deux. Les éboueurs, en particulier ceux qui ont des performances décentes, sont extrêmement complexes et peu de gens les comprennent.

  4. La norme. C ++ inclut le comptage des références (via shared_ptr) et des amis dans la STL, ce qui signifie que la plupart des programmeurs C ++ le connaissent bien et que la plupart du code C ++ l’utilisera. Cependant, il n'y a pas de récupérateur de déchets C ++ standard, ce qui signifie que vous devez en choisir un et espérer que cela fonctionnera bien pour votre cas d'utilisation. Si ce n'est pas le cas, c'est à vous de résoudre le problème, pas à la langue.

En ce qui concerne les inconvénients présumés du comptage de références - ne pas détecter les cycles est un problème, mais je ne l'ai jamais rencontré personnellement au cours des dix dernières années d'utilisation du comptage de références. La plupart des structures de données sont naturellement acycliques, et si vous rencontrez une situation dans laquelle vous avez besoin de références cycliques (par exemple, un pointeur parent dans un nœud d’arbre), vous pouvez simplement utiliser un pointeur C faible ou un C brut pour la "direction arrière". Tant que vous êtes conscient du problème potentiel lors de la conception de vos structures de données, cela ne pose aucun problème.

En ce qui concerne les performances, le comptage de références n’a jamais posé de problème. J'ai eu des problèmes avec les performances de la récupération de place, en particulier avec les gels aléatoires que le GC peut subir, auxquels la seule solution ("ne pas allouer d'objets") pourrait aussi bien être reformulée comme "ne pas utiliser de GC". .

Jeremy Friesner
la source
16
Les implémentations naïves de comptage de références ont généralement un débit beaucoup plus faible que les GC de production (30 à 40%) aux dépens de la latence. Cet écart peut être comblé par des optimisations telles que l’utilisation de moins de bits pour le nombre compté et l’évitement du suivi des objets tant qu’ils ne s’échappent pas. C ++ le fait naturellement si vous revenez principalement make_shared. Néanmoins, la latence est généralement le problème le plus important dans les applications en temps réel, mais le débit est généralement plus important, ce qui explique l’utilisation si répandue des GC. Je ne serais pas si prompt à parler mal d'eux.
Jon Purdy
3
Je chipoterais "plus simple": plus simple en termes de quantité totale d'implémentation requise oui, mais pas plus simple pour le code qui l' utilise : comparer en disant à quelqu'un comment utiliser RC ("faites ceci lors de la création d'objets et ceci lors de leur destruction" ) à comment (naïvement, ce qui est souvent suffisant) utiliser GC ('...').
AakashM
4
"Avec le comptage de références, vous avez la garantie que votre objet sera libéré dès que sa dernière référence disparaît". C'est une idée fausse commune. flyingfrogblog.blogspot.co.uk/2013/10/…
Jon Harrop
4
@JonHarrop: Ce blog-post est terriblement mal dirigé. Vous devriez également lire tous les commentaires, en particulier le dernier.
Déduplicateur
3
@ JonHarrop: Oui, il y en a. Il ne comprend pas que la durée de vie est la portée complète qui va jusqu'à l'accolade fermante. Et l'optimisation dans F # qui, selon les commentaires, ne fonctionne que parfois, termine la durée de vie plus tôt, si la variable n'est pas utilisée à nouveau. Ce qui a naturellement ses propres périls.
Déduplicateur
26

Pour obtenir de bonnes performances d'un CPG, celui-ci doit pouvoir déplacer des objets en mémoire. Dans un langage comme C ++, où vous pouvez interagir directement avec des emplacements de mémoire, cela est pratiquement impossible. (Microsoft C ++ / CLR ne compte pas car il introduit une nouvelle syntaxe pour les pointeurs gérés par le GC et constitue donc un langage différent.)

Le GC Boehm, bien qu’il soit une bonne idée, est en réalité le pire des deux mondes: vous avez besoin d’un malloc () plus lent que le bon GC, et vous perdez ainsi le comportement déterministe d’allocation / désallocation sans l’augmentation correspondante des performances d’un GC générationnel. . De plus, il est par nécessité prudent, de sorte qu'il ne collectera pas nécessairement toutes vos ordures de toute façon.

Un bon GC bien réglé peut être une bonne chose. Mais dans un langage comme le C ++, les gains sont minimes et les coûts souvent tout simplement inutiles.

Il sera intéressant de voir, cependant, que C ++ 11 devient plus populaire, si les lambdas et la sémantique de capture commencent à conduire la communauté C ++ vers les mêmes types de problèmes d’allocation et de durée de vie des objets qui ont amené la communauté Lisp à inventer les GCs dans le premier endroit.

Voir aussi ma réponse à une question connexe sur StackOverflow .

Daniel Pryden
la source
6
RE Boehm GC, je me suis parfois demandé à quel point il était personnellement responsable de l’aversion traditionnelle des programmeurs C et C ++ pour la GC en leur fournissant simplement une mauvaise impression de la technologie en général.
Leushenko
@ Leushenko Bien dit. Un cas typique est cette question, où Boehm gc est appelé un «bon» gc, ignorant le fait que sa fuite est lente et pratiquement garantie. J'ai trouvé cette question en cherchant si quelqu'un avait implémenté un séparateur de cycle de style python pour shared_ptr, ce qui peut sembler être un objectif valable pour une implémentation en c ++.
user4815162342
4

Comme je peux le constater, les pointeurs intelligents sont largement utilisés dans de nombreux projets C ++ réels.

C'est vrai, mais objectivement, la grande majorité du code est maintenant écrit en langage moderne avec le suivi des éboueurs.

Bien que certains pointeurs intelligents soient évidemment bénéfiques pour prendre en charge la RAII et les transferts de propriété, il existe également une tendance à utiliser des pointeurs partagés par défaut, comme moyen de "ramasser les ordures", afin que le programmeur n'ait pas à réfléchir autant à l'allocation. .

C'est une mauvaise idée car vous devez toujours vous soucier des cycles.

Pourquoi les pointeurs partagés sont-ils plus populaires que l'intégration d'un bon ramasse-ordures comme Boehm GC? (Ou êtes-vous d'accord pour dire qu'ils sont plus populaires que les GC actuels?)

Oh wow, il y a tellement de choses qui ne vont pas dans votre façon de penser:

  1. Le GC de Boehm n’est pas un GC "approprié", au sens strict du terme. C'est vraiment affreux. Il est prudent, donc il fuit et est inefficace par conception. Voir: http://flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. Objectivement, les pointeurs partagés sont loin d'être aussi populaires que GC, car la grande majorité des développeurs utilisent maintenant les langages GC et n'ont pas besoin de pointeurs partagés. Il suffit de regarder Java et Javascript sur le marché du travail par rapport au C ++.

  3. Vous semblez limiter votre examen au C ++ parce que, je suppose, vous pensez que le GC est un problème tangentiel. Ce n’est pas le cas (la seule façon d’obtenir un GC décent est de concevoir le langage et la machine virtuelle du début à la fin) de sorte que vous introduisez un biais de sélection. Les personnes qui veulent vraiment une bonne collecte des ordures ne collent pas avec C ++.

Quelles sont les raisons d'utiliser des pointeurs intelligents de comptage de références?

Vous êtes limité à C ++ mais vous souhaitez une gestion automatique de la mémoire.

Jon Harrop
la source
7
Euh, c’est une question balisée c ++ qui parle de fonctionnalités C ++. De toute évidence, les déclarations générales parlent au sein du code de C, et non l'ensemble de la programmation. Donc, si «objectivement» la collecte des ordures peut être utilisée en dehors du monde C ++, cela n’est finalement pas pertinent pour la question à traiter.
Nicol Bolas
2
Votre dernière ligne est manifestement erronée: vous êtes en C ++ et content de ne pas être obligé de traiter avec GC et la libération de ressources est retardée. Il y a une raison pour laquelle Apple n'aime pas GC, et la directive la plus importante pour les langages GC'd est la suivante: Ne créez pas de déchets à moins que vous ne disposiez de nombreuses ressources inactives ou que vous ne puissiez pas vous en empêcher.
Déduplicateur
3
@JonHarrop: comparez donc de petits programmes équivalents avec et sans GC, qui ne sont pas explicitement choisis pour profiter à l'avantage des deux camps. Lequel pensez-vous avoir besoin de plus de mémoire?
Déduplicateur
1
@Déduplicateur: Je peux envisager des programmes qui donnent l'un ou l'autre résultat. Le comptage des références surperformait le traçage du GC lorsque le programme est conçu pour conserver la mémoire allouée en tas tant qu'il ne survit pas à la nursery (par exemple, une file d'attente de listes), car il s'agit d'une performance pathologique pour un GC générationnel et générerait le déchet le plus flottant. Le traçage de la récupération de place nécessiterait moins de mémoire que le comptage de références basé sur la portée lorsqu'il y a de nombreux petits objets et que les durées de vie sont courtes mais pas statiquement bien connues, ce qui ressemble à un programme logique utilisant des structures de données purement fonctionnelles.
Jon Harrop
3
@JonHarrop: Je voulais dire avec GC (traçage ou autre) et RAII si vous parlez C ++. Ce qui inclut le comptage de références, mais uniquement là où cela est utile. Ou vous pouvez comparer avec un programme Swift.
Déduplicateur
3

Sous MacOS X et iOS, ainsi que chez les développeurs utilisant Objective-C ou Swift, le comptage des références est populaire car il est géré automatiquement, et l'utilisation de la récupération de place a considérablement diminué, car Apple ne la prend plus en charge (on me dit que les applications utilisant Le ramassage des ordures va casser dans la prochaine version de MacOS X et le ramassage des ordures n’a jamais été implémenté dans iOS). En fait, je doute sérieusement qu’il y ait jamais eu beaucoup de logiciels utilisant la collecte des ordures lorsqu’ils étaient disponibles.

La raison pour se débarrasser de la récupération de place: cela n’a jamais fonctionné de manière fiable dans un environnement de style C où les pointeurs pourraient «s’échapper» vers des zones non accessibles au récupérateur de place. Apple croyait fermement que le comptage des références est plus rapide. (Vous pouvez faire ici des déclarations sur la vitesse relative, mais personne n’a réussi à convaincre Apple). Et finalement, personne n’a utilisé le ramassage des ordures.

La première chose que tout développeur MacOS X ou iOS apprend, c'est comment gérer les cycles de référence. Ce n'est donc pas un problème pour un vrai développeur.

gnasher729
la source
Si je comprends bien, ce n’est pas un environnement de type C qui décide, mais le GC est indéterministe et nécessite beaucoup plus de mémoire pour obtenir des performances acceptables, et le serveur / le poste de travail extérieur est toujours un peu rare.
Déduplicateur
Le débogage de la raison pour laquelle le ramasse-miettes a détruit un objet que j'utilisais toujours (conduisant à un crash) l'a décidé pour moi :-)
gnasher729
Oh oui, ça le ferait aussi. Avez-vous finalement compris pourquoi?
Déduplicateur
Oui, c’était l’une des nombreuses fonctions Unix dans lesquelles vous transmettez un vide * en tant que "contexte" qui vous est ensuite rendu dans une fonction de rappel; le vide * était vraiment un objet Objective-C, et le ramasse-miettes n'a pas réalisé que l'objet était stocké dans l'appel Unix. Le rappel est appelé, jette void * to Object *, kaboom!
gnasher729
2

Le plus gros inconvénient de la récupération de place en C ++ est qu’il est impossible d’obtenir une réponse exacte:

  • En C ++, les pointeurs ne résident pas dans leur propre communauté murée, ils sont mélangés à d'autres données. En tant que tel, vous ne pouvez pas distinguer un pointeur d'autres données dont le motif binaire peut être interprété comme un pointeur valide.

    Conséquence: tout le ramasse-miettes C ++ lâchera des objets à collecter.

  • En C ++, vous pouvez faire de l'arithmétique de pointeur pour dériver des pointeurs. En tant que tel, si vous ne trouvez pas de pointeur sur le début d'un bloc, cela ne signifie pas que ce bloc ne peut pas être référencé.

    Conséquence: tout ramasse-miettes C ++ doit prendre en compte ces ajustements, en considérant toute séquence de bits qui pointe n'importe où dans un bloc, y compris juste après sa fin, comme un pointeur valide référençant le bloc.

    Remarque: Aucun récupérateur de mémoire C ++ ne peut gérer du code avec des astuces comme celles-ci:

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

    Certes, cela invoque un comportement indéfini. Mais certains codes existants sont plus intelligents que bons, et ils peuvent déclencher une désallocation préliminaire par un ramasse-miettes.

cmaster
la source
2
" Ils sont mélangés avec d'autres données. " Ce n'est pas tellement qu'ils sont "mélangés" avec d'autres données. Il est facile d'utiliser le système de types C ++ pour voir ce qui est un pointeur et ce qui ne l'est pas. Le problème est que les pointeurs deviennent souvent d' autres données. Cacher un pointeur dans un entier est malheureusement un outil commun à de nombreuses API de style C.
Nicol Bolas
1
Vous n'avez même pas besoin d'un comportement indéfini pour bousiller un ramasse-miettes en c ++. Vous pouvez, par exemple, sérialiser un pointeur sur un fichier et le lire ultérieurement. Dans l'intervalle, votre processus peut ne pas contenir ce pointeur à un endroit quelconque de son espace d'adressage. Le garbage collector peut alors collecter cet objet, puis lorsque vous désérialisez le pointeur ... Oups.
Bwmat
@Bwmat "Même"? Écrire des pointeurs sur un fichier comme celui-là semble un peu ... tiré par les cheveux. Quoi qu'il en soit, le même problème sérieux affecte les pointeurs à l'empilement d'objets. Ils peuvent disparaître lorsque vous relisez le pointeur à partir d'un fichier ailleurs dans le code! Désérialiser une valeur de pointeur invalide est un comportement indéfini, ne le faites pas.
Hyde
Bien sûr, vous devez faire attention si vous faites quelque chose comme ça. Cela devait être un exemple du fait qu'en général, un ramasse-miettes ne peut pas fonctionner "correctement" dans tous les cas en c ++ (sans changer de langue)
Bwmat le
1
@ gnasher729: Euh, non? Les pointeurs passés sont parfaits?
Déduplicateur