Pourquoi la collecte des ordures lorsque RAII est disponible?

J’entends parler de C ++ 14 en introduisant un ramasse-miettes dans la bibliothèque standard C ++. Quelle est la raison d’être de cette fonctionnalité? N’est-ce pas la raison pour laquelle RAII existe en C ++?

  • Comment la présence du ramasse-miettes de la bibliothèque standard affectera-t-elle la sémantique RAII?
  • Comment est-ce important pour moi (le programmeur) ou la façon dont j’écris des programmes C ++?

La collecte des ordures et RAII sont utiles dans différents contextes. La présence de GC ne devrait pas affecter votre utilisation de RAII. Le RAII étant bien connu, je donne deux exemples où le GC est pratique.


La récupération de la mémoire serait très utile pour mettre en œuvre des structures de données sans locking.

[…] il s’avère que la libération de mémoire déterministe est un problème fondamental dans les structures de données sans locking. (à partir de structures de données sans locking par Andrei Alexandrescu)

Fondamentalement, le problème est que vous devez vous assurer que vous ne désallouez pas la mémoire lorsqu’un thread le lit. C’est là que GC devient pratique: il peut regarder les threads et ne faire que la désallocation quand elle est sûre. Veuillez lire l’article pour plus de détails.

Juste pour être clair ici: cela ne signifie pas que WHOLE WORLD devrait être collecté comme en Java; seules les données pertinentes doivent être collectées avec précision.


Dans une de ses présentations, Bjarne Stroustrup a également donné un bon exemple valable où GC devient pratique. Imaginez une application écrite en C / C ++, d’une taille de 10 Mo SLOC. L’application fonctionne raisonnablement bien (sans bug) mais elle fuit. Vous n’avez ni les ressources (heures de main-d’œuvre) ni les connaissances fonctionnelles pour résoudre ce problème. Le code source est un code hérité quelque peu désordonné. Que faire? Je suis d’accord que c’est peut-être le moyen le plus facile et le moins coûteux de résoudre le problème avec GC.


Comme cela a été signalé par sasha.sochka , le ramassemiettes sera facultatif .

Ma préoccupation personnelle est que les gens commenceraient à utiliser GC comme il est utilisé en Java et écriraient du code malpropre et que tout serait ramassé. (J’ai l’impression que shared_ptr est déjà devenu la valeur par défaut “aller à”, même dans les cas où l’allocation de stack unique_ptr ou, hell, le ferait.)

Je suis d’accord avec @DeadMG qu’il n’y a pas de GC dans la norme C ++ actuelle, mais j’aimerais append la citation suivante de B. Stroustrup:

Lorsque (pas si) la récupération automatique de la mémoire devient partie intégrante de C ++, ce sera facultatif.

Donc, Bjarne est sûr qu’il sera ajouté à l’avenir. Au moins le président du GTE (groupe de travail sur l’évolution) et l’un des membres les plus importants du comité (et surtout le créateur de la langue) souhaite l’append.

À moins qu’il n’ait changé d’avis, nous pouvons nous attendre à ce qu’il soit ajouté et mis en œuvre à l’avenir.

Il existe des algorithmes compliqués / inefficaces / impossibles à écrire sans un GC. Je soupçonne que c’est le principal argument de vente de GC en C ++, et qu’il ne peut jamais être utilisé comme un allocateur à usage général.

Pourquoi pas un allocateur polyvalent?

Premièrement, nous avons RAII et la plupart (y compris moi-même) semblent croire qu’il s’agit d’une méthode supérieure de gestion des ressources. Nous aimons le déterminisme car il simplifie beaucoup l’écriture de code robuste et sans fuites et rend les performances prévisibles.

Deuxièmement, vous devrez imposer des ressortingctions très peu compatibles avec C ++ sur la manière d’utiliser la mémoire. Par exemple, vous avez besoin d’au moins un pointeur accessible et non masqué. Les pointeurs obscurcis, tels que ceux couramment utilisés dans les bibliothèques de conteneurs arborescentes communes (utilisant des bits bas garantis par l’alignement pour les indicateurs de couleur), ne seront pas reconnaissables par le CPG.

En relation avec cela, les choses qui rendent les CGs modernes si utilisables seront très difficiles à appliquer au C ++ si vous supportez un nombre quelconque de pointeurs obscurcis. La défragmentation générationnelle des GC est vraiment cool, car l’allocation est extrêmement peu coûteuse (essentiellement en incrémentant simplement un pointeur) et vos allocations finissent par se réduire à quelque chose de plus petit avec une localité améliorée. Pour ce faire, les objects doivent être mobiles.

Pour rendre un object déplaçable en toute sécurité, le GC doit pouvoir mettre à jour tous les indicateurs. Il ne sera pas capable de trouver des obfusqués. Cela pourrait être adapté, mais ne serait pas joli (probablement un type gc_pin ou similaire, utilisé comme le std::lock_guard , qui est utilisé chaque fois que vous avez besoin d’un pointeur brut). La convivialité serait à la porte.

Sans rendre les choses mobiles, un GC serait nettement plus lent et moins évolutif que ce à quoi vous êtes habitué ailleurs.

Des raisons d’utilisabilité (gestion des ressources) et des raisons d’efficacité (allocations rapides et mobiles) sont hors de question, à quoi sert le GC? Certainement pas polyvalent. Entrez des algorithmes sans verrou.

Pourquoi sans verrou?

Les algorithmes sans verrou fonctionnent en permettant à une opération en conflit de se “désynchroniser” temporairement de la structure de données et de la détecter / corriger ultérieurement. Un des effets est que la mémoire sous contention peut être accédée après sa suppression. Par exemple, si vous avez plusieurs threads en compétition pour faire apparaître un nœud depuis un LIFO, il est possible qu’un thread fasse apparaître et supprime le nœud avant qu’un autre thread ne réalise que le nœud a déjà été pris:

Fil A:

  • Récupère le pointeur sur le nœud racine.
  • Récupère le pointeur sur le nœud suivant depuis le nœud racine.
  • Suspendre

Fil B:

  • Récupère le pointeur sur le nœud racine.
  • Suspendre

Fil A:

  • Noeud pop. (remplacez le pointeur du nœud racine par le prochain pointeur, si le pointeur du nœud racine n’a pas changé depuis sa lecture).
  • Supprimer le noeud.
  • Suspendre

Fil B:

  • Obtenez le pointeur sur le prochain nœud de notre pointeur de nœud racine, qui est maintenant “désynchronisé” et qui vient d’être supprimé.

Avec GC, vous pouvez éviter la possibilité de lire dans la mémoire non validée car le nœud ne sera jamais supprimé lorsque Thread B le référence. Il existe des moyens de contourner ce problème, tels que les indicateurs de risque ou les exceptions SEH sous Windows, mais ils peuvent nuire considérablement aux performances. GC a tendance à être la solution la plus optimale ici.

Il n’y en a pas, car il n’y en a pas. Les seules fonctionnalités que C ++ a jamais eues pour GC ont été introduites dans C ++ 11 et elles ne font que marquer la mémoire, aucun collecteur n’est requirejs. Il n’y aura pas non plus de C ++ 14.

Selon moi, il n’ya aucun moyen pour un collectionneur de passer le comité.

Aucune des réponses apscopes jusqu’ici ne concerne l’avantage le plus important de l’ajout de la corbeille dans un langage: en l’absence de récupération de mémoire prise en charge par le langage, il est presque impossible de garantir qu’aucun object ne sera détruit. Pire encore, si une telle chose se produit, il est presque impossible de garantir qu’une tentative ultérieure d’utilisation de la référence ne finira pas par manipuler un autre object aléatoire.

Bien qu’il existe de nombreux types d’objects dont la durée de vie peut être bien mieux gérée par RAII que par un ramasse-miettes, le fait que le GC gère presque tous les objects, y compris ceux dont la durée de vie est contrôlée par RAII , présente un intérêt considérable. Le destructeur d’un object devrait tuer l’object et le rendre inutile, mais laisser le cadavre derrière le GC. Toute référence à l’object deviendra ainsi une référence au cadavre et le restra jusqu’à ce que (la référence) cesse complètement d’exister. Ce n’est que lorsque toutes les références au corps ont cessé d’exister que le corps lui-même le fera.

Bien qu’il existe des moyens d’implémenter des ramasse-miettes sans prise en charge de langage inhérente, de telles implémentations exigent que le GC soit informé à chaque fois que des références sont créées ou détruites (ajout de tracas et surcharge considérables), about pourrait exister à un object qui est autrement non référencé. Le support du compilateur pour GC élimine ces deux problèmes.

GC présente les avantages suivants:

  1. Il peut gérer des références circulaires sans aide du programmeur (avec le style RAII, vous devez utiliser faibles_ptr pour casser les cercles). Ainsi, une application de style RAII peut toujours “fuir” si elle est utilisée de manière incorrecte.
  2. Créer / détruire des tonnes de shared_ptr sur un object donné peut être coûteux, car l’incrément / décrément de refcount sont des opérations atomiques. Dans les applications multithread, les emplacements de mémoire contenant des refcounts seront des emplacements «chauds», ce qui mettra beaucoup de pression sur le sous-système de mémoire. GC n’est pas enclin à ce problème spécifique, car il utilise des ensembles accessibles au lieu de refcounts.

Je ne dis pas que GC est le meilleur / bon choix. Je dis juste que cela a des caractéristiques différentes. Dans certains scénarios, cela pourrait être un avantage.

Définitions:

RCB GC: GC basé sur le comptage de référence.

MSB GC: GC basé sur le Mark-Sweep.

Réponse rapide:

MSB GC doit être ajouté au standard C ++, car il est plus pratique que le GC RCB dans certains cas.

Deux exemples illustratifs:

Considérons un tampon global dont la taille initiale est petite, et tout thread peut agrandir dynamicment sa taille et garder l’ancien contenu accessible pour les autres threads.

Mise en œuvre 1 (version GC du MSB):

 int* g_buf = 0; size_t g_current_buf_size = 1024; void InitializeGlobalBuffer() { g_buf = gcnew int[g_current_buf_size]; } int GetValueFromGlobalBuffer(size_t index) { return g_buf[index]; } void EnlargeGlobalBufferSize(size_t new_size) { if (new_size > g_current_buf_size) { auto tmp_buf = gcnew int[new_size]; memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int)); std::swap(tmp_buf, g_buf); } } 

Mise en œuvre 2 (version GC RCB):

 std::shared_ptr g_buf; size_t g_current_buf_size = 1024; std::shared_ptr NewBuffer(size_t size) { return std::shared_ptr(new int[size], []( int *p ) { delete[] p; }); } void InitializeGlobalBuffer() { g_buf = NewBuffer(g_current_buf_size); } int GetValueFromGlobalBuffer(size_t index) { return g_buf[index]; } void EnlargeGlobalBufferSize(size_t new_size) { if (new_size > g_current_buf_size) { auto tmp_buf = NewBuffer(new_size); memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int)); std::swap(tmp_buf, g_buf); // // Now tmp_buf owns the old g_buf, when tmp_buf is destructed, // the old g_buf will also be deleted. // } } 

NOTEZ S’IL VOUS PLAÎT:

Après avoir appelé std::swap(tmp_buf, g_buf); , tmp_buf possède l’ancien g_buf . Lorsque tmp_buf est détruit, l’ancien g_buf sera également supprimé.

Si un autre thread appelle GetValueFromGlobalBuffer(index); pour récupérer la valeur de l’ancien g_buf , alors un risque de course se produira !!!

Donc, bien que l’implémentation 2 soit aussi élégante que l’implémentation 1, cela ne fonctionne pas!

Si nous voulons que l’implémentation 2 fonctionne correctement, nous devons append une sorte de mécanisme de locking; alors ce sera non seulement plus lent, mais moins élégant que la mise en œuvre 1.

Conclusion:

Il est bon de prendre MSB GC dans le standard C ++ en tant que fonctionnalité facultative.