Comment un ramasse-miettes empêche-t-il l'analyse de la mémoire entière à chaque collecte?


16

Certains (au moins Mono et .NET) collecteurs de déchets ont une zone de mémoire à court terme qu'ils analysent souvent et une zone de mémoire secondaire qu'ils analysent moins souvent. Mono appelle cela une pépinière.

Pour savoir quels objets peuvent être supprimés, ils analysent tous les objets à partir des racines, de la pile et des registres et éliminent tous les objets qui ne sont plus référencés.

Ma question est de savoir comment ils empêchent toute la mémoire en cours d'utilisation d'être analysée à chaque collecte? En principe, la seule façon de savoir quels objets ne sont plus utilisés est de scanner tous les objets et toutes leurs références. Cependant, cela empêcherait le système d'exploitation d'échanger de la mémoire même si elle n'est pas utilisée par l'application et semble être une énorme quantité de travail à faire, également pour "Collection Nursery". Il ne semble pas qu'ils gagnent beaucoup en utilisant une pépinière.

Suis-je en train de manquer quelque chose ou le ramasse-miettes analyse-t-il réellement chaque objet et chaque référence chaque fois qu'il effectue une collecte?


1
un bel aperçu est dans un article The Art of Garbage Collection Tuning écrit par Angelika Langer. Formellement, il s'agit de la façon dont cela se fait en Java, mais les concepts présentés sont à peu près indépendants du langage
gnat

Réponses:


14

Les observations fondamentales qui permettent un ramassage des ordures générationnel pour éviter d'avoir à analyser tous les objets de génération plus ancienne sont:

  1. Après une collection, tous les objets qui existent encore seront d'une génération minimale (par exemple dans .net, après une collection Gen0, tous les objets sont Gen1 ou Gen2; après une collection Gen1 ou Gen2, tous les objets sont Gen2).
  2. Un objet, ou une partie de celui-ci, qui n'a pas été écrit depuis une collection qui a tout promu à la génération N ou supérieure ne peut contenir aucune référence à des objets de générations inférieures.
  3. Si un objet a atteint une certaine génération, il n'est pas nécessaire de l'identifier comme accessible pour garantir sa rétention lors de la collecte des générations inférieures.

Dans de nombreux frameworks GC, il est possible que le garbage collector marque des objets ou des parties de ceux-ci de telle sorte que la première tentative d'écriture sur eux déclenche un code spécial pour enregistrer le fait qu'ils ont été modifiés. Un objet ou une partie de celui-ci qui a été modifié, quelle que soit sa génération, doit être analysé dans la collection suivante, car il peut contenir des références à des objets plus récents. D'un autre côté, il est très courant qu'il y ait beaucoup d'objets plus anciens qui ne soient pas modifiés entre les collections. Le fait que les analyses de génération inférieure peuvent ignorer de tels objets peut permettre à ces analyses de se terminer beaucoup plus rapidement qu’elles ne le feraient autrement.

Notez, btw, que même si l'on ne peut pas détecter lorsque des objets sont modifiés et qu'il faudrait tout analyser à chaque passage du GC, la récupération de place générationnelle pourrait encore améliorer les performances de l'étape de "balayage" d'un collecteur de compactage. Dans certains environnements intégrés (en particulier ceux où il y a peu ou pas de différence de vitesse entre les accès séquentiels et aléatoires à la mémoire), le déplacement de blocs de mémoire est relativement coûteux par rapport au balisage de références. Par conséquent, même si la phase de «marquage» ne peut pas être accélérée à l'aide d'un collecteur générationnel, l'accélération de la phase de «balayage» peut être utile.


déplacer des blocs de mémoire coûte cher dans n'importe quel système, donc l'amélioration du balayage est un gain même sur votre système CPU quad Ghz.
gbjbaanb

@gbjbaanb: Dans de nombreux cas, le coût de tout scanner pour trouver des objets vivants serait important et répréhensible même si le déplacement des objets était totalement gratuit. Par conséquent, il convient, dans la mesure du possible, d'éviter de numériser de vieux objets. D'un autre côté, s'abstenir de compacter des objets plus anciens est une optimisation simple qui peut être accomplie même sur des cadres simples. BTW, si l'on concevait un framework GC pour un petit système embarqué, la prise en charge déclarative des objets immuables pourrait être utile. Il est difficile de savoir si un objet modifiable a changé, mais on pourrait bien faire ...
supercat

... supposons simplement que les objets mutables doivent être scannés à chaque passage du GC, mais pas les objets immuables. Même si la seule façon de construire un objet immuable était de construire un "prototype" dans un espace mutable puis de le copier, la seule opération de copie supplémentaire pourrait éviter d'avoir à scanner l'objet lors des futures opérations GC.
supercat

Soit dit en passant, les performances de collecte des ordures sur les implémentations dérivées de Microsoft des années 1980 de BASIC pour 6502 microprocesseurs (et peut-être d'autres aussi) pourraient être considérablement améliorées dans certains cas, si un programme qui générait beaucoup de chaînes qui ne changeraient jamais, copiait le "suivant allocation de chaîne "pointeur vers le pointeur" haut de l'espace de chaîne ". Un tel changement empêcherait le garbage collector d'examiner l'une des anciennes chaînes pour voir si elles étaient toujours nécessaires. Le Commodore 64 n'était guère de haute technologie, mais un tel GC «générationnel» y aiderait même.
supercat

7

Les GC auxquels vous faites référence sont des récupérateurs de génération . Ils sont conçus pour tirer le meilleur parti d'une observation connue sous le nom de «mortalité infantile» ou «hypothèse générationnelle», ce qui signifie que la plupart des objets deviennent inaccessibles très rapidement. Ils scannent en effet à partir des racines, mais ignorent tous les anciens objets . Par conséquent, ils n'ont pas besoin de scanner la plupart des objets en mémoire, ils ne scannent que les jeunes objets (au détriment de ne pas détecter les vieux objets inaccessibles, du moins pas à ce stade).

"Mais c'est faux", je vous entends crier, "les objets anciens peuvent et font référence à de jeunes objets". Vous avez raison, et il existe plusieurs solutions à cela, qui tournent toutes autour de l'acquisition de connaissances, rapidement et efficacement, quels objets anciens doivent être vérifiés et qui peuvent être ignorés en toute sécurité. Ils se résument à des objets d'enregistrement ou à de petites plages de mémoire (plus grandes que les objets, mais beaucoup plus petites que l'ensemble) qui contiennent des pointeurs vers les générations plus jeunes. D'autres ont décrit ceux-ci bien mieux que moi, je vais donc vous donner quelques mots-clés: marquage de carte, jeux mémorisés, barrières d'écriture. Il existe également d'autres techniques (y compris les hybrides), mais celles-ci englobent les approches courantes que je connais.


3

Pour savoir quels objets de la pépinière sont encore en vie, le collecteur n'a qu'à scanner le jeu de racines et tous les anciens objets qui ont été mutés depuis la dernière collection , car un vieil objet qui n'a pas été récemment muté ne peut pas pointer vers un jeune objet . Il existe différents algorithmes pour maintenir ces informations à différents niveaux de précision (d'un ensemble exact de champs mutés à un ensemble de pages où une mutation peut s'être produite), mais ils impliquent généralement une sorte de barrière d'écriture : un code qui s'exécute sur chaque référence -mutation de champ typée qui met à jour la comptabilité du GC.


1

La génération la plus ancienne et la plus simple de ramasse-miettes a effectivement analysé toute la mémoire et a dû arrêter tous les autres traitements pendant qu'elle le faisait. Les algorithmes ultérieurs se sont améliorés sur ce point de diverses manières - rendant la copie / numérisation incrémentielle ou exécutée en parallèle. La plupart des collecteurs d'ordures modernes séparent les objets en générations et gèrent soigneusement les pointeurs intergénérationnels afin que les nouvelles générations puissent être collectées sans déranger les plus anciennes.

Le point clé est que les garbage collector travaillent en étroite collaboration avec le compilateur et avec le reste du runtime pour maintenir l'illusion qu'il surveille toute la mémoire.


Je ne sais pas quelles approches de récupération de place ont été utilisées dans les mini-ordinateurs et les ordinateurs centraux avant la fin des années 1970, mais le garbage collector Microsoft BASIC, au moins sur les machines 6502, définirait son pointeur "chaîne suivante" en haut de la mémoire, puis rechercher toutes les références de chaîne pour trouver l'adresse la plus élevée qui était en dessous du "pointeur de chaîne suivant". Cette chaîne serait copiée juste en dessous du "pointeur de chaîne suivant", et ce pointeur serait parqué juste en dessous. L'algorithme se répéterait alors. Il était possible pour le code de bloquer les pointeurs à fournir ...
supercat

... quelque chose comme une collection générationnelle. Je me suis parfois demandé à quel point il serait difficile de patcher le BASIC pour implémenter la collection "générationnelle" en gardant simplement les adresses du haut de chaque génération, et en ajoutant quelques opérations de permutation de pointeurs avant et après chaque cycle de GC. Les performances du GC seraient encore assez mauvaises, mais pourraient dans de nombreux cas être réduites de quelques dizaines à dixièmes de secondes.
supercat

-2

Fondamentalement ... GC utilise des "compartiments" pour séparer ce qui est utilisé et ce qui ne l'est pas. Une fois qu'il le fait vérifier, il efface les choses qui ne sont pas utilisées et déplace tout le reste vers la 2e génération (qui est vérifié moins souvent que la 1re génération), puis déplace les choses qui sont encore en cours d'utilisation dans la 2e den à la 3e génération.

Donc, les choses de la 3e génération sont généralement des objets qui sont bloqués pour une raison quelconque, et GC n'y vérifie pas très souvent.


1
Mais comment sait-il quels objets sont utilisés?
Pieter van Ginkel

Il garde la trace des objets accessibles à partir du code accessible. Une fois qu'un objet n'est plus accessible à partir d'un code pouvant s'exécuter (par exemple, du code pour une méthode qui est revenue), le GC sait qu'il est sûr de le collecter
JohnL

Vous décrivez tous les deux comment les GC sont corrects, pas comment ils sont efficaces. À en juger par la question, OP le sait très bien.

@delnan yes Je répondais à la question de savoir comment il sait quels objets sont utilisés, ce qui était le commentaire de Pieter.
JohnL

-5

L'algorithme habituellement utilisé par ce GC est le mark-and-sweep naïf

vous devez également être conscient du fait que ce n'est pas géré par le C # lui-même, mais par le soi-disant CLR .


C'est le sentiment que j'ai ressenti en lisant sur le ramasse-miettes de Mono. Cependant, ce que je ne comprends pas, c'est pourquoi s'ils analysent l'ensemble de travail complet sur jamais collecter, ils ont un collecteur générationnel avec lequel la collection GEN-0 est très rapide. Comment cela peut-il être rapide avec un jeu de disons 2 Go?
Pieter van Ginkel

eh bien, le vrai GC pour mono est Sgen, vous devriez lire ce mono-project.com/Generational_GC ou quelques articles en ligne schani.wordpress.com/tag/mono infoq.com/news/2011/01/SGen , le fait est que ces nouvelles technologies comme CLR et CLI ont une conception vraiment modulaire, le langage devient juste un moyen d'exprimer quelque chose pour le CLR et non un moyen de produire du code binaire. Votre question concerne les détails de l'implémentation et non les algorithmes, car un algorithme n'a toujours pas d'implémentation, vous devriez simplement lire les articles techniques et les articles de Mono, personne d'autre.
user827992

Je suis confus. La stratégie utilisée par un garbage collector n'est pas un algorithme?
Pieter van Ginkel

2
-1 Arrêtez de confondre OP. Que le GC fasse partie du CLR et ne soit pas spécifique à la langue n'est pas du tout pertinent. Un GC est principalement caractérisé par la façon dont elle dispose le tas et détermine l' accessibilité, et celui - ci est tout à propos de l'algorithme utilisé (s) pour cela. Bien qu'il puisse y avoir de nombreuses implémentations d'un algorithme et que vous ne devriez pas vous laisser entraîner dans les détails de l'implémentation, l'algorithme seul détermine le nombre d'objets à analyser. Un GC générationnel est simplement un algorithme + une disposition en tas qui tente d'utiliser "l'hypothèse générationnelle" (que la plupart des objets meurent jeunes). Ce ne sont pas naïfs.

4
Algorithme! = Implémentation en effet, mais une implémentation ne peut que dévier aussi loin avant de devenir une implémentation d'un algorithme différent. Une description d'algorithme, dans le monde GC, est très spécifique et inclut des choses comme le fait de ne pas balayer tout le tas sur la collection de pépinière et comment les pointeurs intergénérationnels sont trouvés et stockés. Il est vrai qu'un algorithme ne vous dit pas combien de temps une étape spécifique de l'algorithme prendra, mais ce n'est pas du tout pertinent pour cette question.
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.