web-dev-qa-db-fra.com

Comment fonctionne le ramasse-miettes dans les langues qui sont compilées en mode natif?

Après avoir parcouru plusieurs réponses à un débordement de pile, il est clair que certaines langues compilées nativement ont un ramasse-miettes . Mais je ne sais pas exactement comment cela fonctionnerait.

Je comprends comment la collecte des ordures peut fonctionner avec un langage interprété. Le garbage collector fonctionnerait simplement à côté de l'interpréteur et supprimerait les objets inutilisés et inaccessibles de la mémoire du programme. Ils courent tous les deux ensemble.

Comment cela fonctionnerait-il avec les langues compilées? Ma compréhension est qu'une fois que le compilateur a compilé le code source dans le code cible - spécifiquement code machine natif - c'est fait. Son travail est terminé. Alors, comment le programme compilé pourrait-il également être récupéré?

Le compilateur fonctionne-t-il d'une manière ou d'une autre pendant que le programme est exécuté pour supprimer les objets "poubelle"? Ou le compilateur inclut-il un récupérateur de place minimal dans l'exécutable du programme compilé.

Je crois que ma dernière déclaration aurait plus de validité que la précédente en raison de cet extrait de cette réponse sur Stack Overflow :

Un de ces langages de programmation est Eiffel. La plupart des compilateurs Eiffel génèrent du code C pour des raisons de portabilité. Ce code C est utilisé pour produire du code machine par un compilateur C standard. Les implémentations Eiffel fournissent GC (et parfois même GC précis) pour ce code compilé, et il n'y a pas besoin de VM. En particulier, le compilateur VisualEiffel a généré directement du code machine natif x86 avec une prise en charge complète du GC .

La dernière instruction semble impliquer que le compilateur inclut un programme dans l'exécutable final qui agit comme un garbage collector pendant l'exécution du programme.

La page sur le site Web du langage D sur le ramasse-miettes - qui est compilé en mode natif et possède un ramasse-miettes en option - semble également suggérer que certains programmes d'arrière-plan fonctionnent avec le programme exécutable d'origine pour implémenter le ramasse-miettes.

D est un langage de programmation système prenant en charge la récupération de place. Il n'est généralement pas nécessaire de libérer de la mémoire de manière explicite. Allouez simplement selon les besoins et le garbage collector retournera périodiquement toute la mémoire inutilisée au pool de mémoire disponible.

Si la méthode mentionnée ci-dessus est utilisée, comment fonctionnerait-elle exactement? Le compilateur stocke-t-il une copie d'un programme de récupération de place et la colle-t-il dans chaque exécutable qu'il génère?

Ou suis-je défectueux dans ma pensée? Si tel est le cas, quelles méthodes sont utilisées pour implémenter le garbage collection pour les langues compilées et comment fonctionneraient-elles exactement?

80
Christian Dean

Le garbage collection dans un langage compilé fonctionne de la même manière que dans un langage interprété. Des langages comme Go utilisent le traçage des récupérateurs même si leur code est généralement compilé à l'avance pour coder le code machine.

(Tracing) garbage collection commence généralement par parcourir les piles d'appels de tous les threads en cours d'exécution. Les objets sur ces piles sont toujours vivants. Après cela, le garbage collector traverse tous les objets qui sont pointés par des objets en direct, jusqu'à ce que le graphique de l'objet en direct soit entièrement découvert.

Il est clair que cela nécessite des informations supplémentaires que les langages comme C ne fournissent pas. En particulier, il nécessite une carte du cadre de pile de chaque fonction qui contient les décalages de tous les pointeurs (et probablement leurs types de données) ainsi que des cartes de toutes les dispositions d'objets qui contiennent les mêmes informations.

Il est cependant facile de voir que les langages qui ont de fortes garanties de type (par exemple si les conversions de pointeurs vers différents types de données sont interdits) peuvent en effet calculer ces cartes au moment de la compilation. Ils stockent simplement une association entre les adresses d'instructions et les cartes de trame de pile et une association entre les types de données et les cartes de disposition d'objet à l'intérieur du binaire. Ces informations leur permettent ensuite de parcourir le graphe d'objets.

Le garbage collector lui-même n'est rien de plus qu'une bibliothèque liée au programme, similaire à la bibliothèque standard C. Par exemple, cette bibliothèque pourrait fournir une fonction similaire à malloc() qui exécute l'algorithme de collecte si la pression mémoire est élevée.

54
avdgrinten

Le compilateur stocke-t-il une copie d'un programme de récupération de place et la colle-t-il dans chaque exécutable qu'il génère?

Cela semble non élégant et bizarre, mais oui. Le compilateur possède une bibliothèque d'utilitaires complète, contenant bien plus que du code de récupération de place, et les appels à cette bibliothèque seront insérés dans chaque exécutable qu'il crée. C'est ce qu'on appelle la bibliothèque d'exécution , et vous seriez surpris du nombre de tâches différentes qu'elle sert généralement.

122
Kilian Foth

Ou le compilateur inclut-il un récupérateur de place minimal dans le code du programme compilé.

C’est une façon étrange de dire "le compilateur relie le programme à une bibliothèque qui effectue la collecte des ordures". Mais oui, c'est ce qui se passe.

Cela n'a rien de spécial: les compilateurs lient généralement tonnes des bibliothèques dans les programmes qu'ils compilent; sinon, les programmes compilés ne pourraient pas faire grand-chose sans réimplémenter beaucoup de choses à partir de zéro: même écrire du texte à l'écran/un fichier /… nécessite une bibliothèque.

Mais peut-être que GC est différent de ces autres bibliothèques, qui fournissent des API explicites que l'utilisateur appelle?

Non: dans la plupart des langues, les bibliothèques d'exécution font beaucoup de travail en arrière-plan sans API publique, au-delà de GC. Considérez ces trois exemples:

  1. Propagation des exceptions et déroulement de la pile/appel du destructeur.
  2. Allocation dynamique de mémoire (qui n’appelle généralement pas seulement une fonction, comme en C, même quand il n’y a pas de garbage collection).
  3. Suivi des informations de type dynamique (pour les transtypages, etc.).

Donc, une bibliothèque de récupération de place n'est pas du tout spéciale, et a priori n'a rien à voir avec le fait qu'un programme ait été compilé à l'avance.

58
Konrad Rudolph

Comment cela fonctionnerait-il avec les langues compilées?

Votre formulation est fausse. Un langage de programmation est unspécificationécrit dans un rapport technique (pour un bon exemple, voir R5RS ). En fait, vous faites référence à certainsspécifiqueslangueimplémentation(qui est un logiciel).

(certains langages de programmation ont de mauvaises spécifications, voire des manquants, ou tout aussi conformes à un exemple d'implémentation; encore, un langage de programmation définit uncomportement- par exemple il a un syntaxe et sémantique -, c'estpasun produit logiciel, mais pourrait êtreimplémentépar certains logiciels; de nombreux langages de programmation ontplusieursimplémentations; en particulier, "compilé" est un adjectif s'appliquant àimplémentations- même si certains langages de programmation sont plus faciles à implémenter par les interprètes que par les compilateurs.)

Ma compréhension est qu'une fois que le compilateur a compilé le code source dans le code cible - spécifiquement le code machine natif - c'est fait. Son travail est terminé.

Notez que les interprètes et les compilateurs ont un sens vague et que certaines implémentations de langage peuvent être considérées comme étant les deux. En d'autres termes, il existe un continuum entre les deux. Lisez le dernier Dragon Book et pensez à bytecode , Compilation JIT ,dynamiquementémettant du code C qui est compilé dans un "plugin" puis dlopen (3) - ed par le même processus (et sur les machines actuelles, c'est assez rapide pour être compatible avec une réplique interactive , voir this )


Je recommande fortement de lire le manuel GC . Un livre entier est nécessaire pour répondre . Avant cela, lisez le Garbage Collection wikipage (que je suppose que vous avez lu avant de lire ci-dessous).

Le système runtime de l'implémentation du langage compilé contient le garbage collector, et le compilateur génère du code qui estfitpour ce système d'exécution particulier . En particulier, les primitives d'allocation (sont compilées en code machine qui) appellent (ou peuvent) appeler le système d'exécution.

Alors, comment le programme compilé pourrait-il également être récupéré?

Juste en émettant du code machine qui utilise (et est "convivial" et "compatible avec") le système d'exécution.

Notez que vous pouvez trouver plusieurs bibliothèques de récupération de place, en particulier Boehm GC , MPS de Ravenbrook , ou même mon (non entretenu) Qish . Et le codage d'unsimpleGC n'est pas très difficile (cependant, le débogage est plus difficile, et le codage d'unconcurrentielGC estdifficile).

Dans certains cas, le compilateur utilise unconservateurGC (comme Boehm GC ). Ensuite, il n'y a pas grand chose à coder. Le GC conservateur (lorsque le compilateur appelle sa routine d'allocation, ou la routine GC entière) parfoisscanla totalité pile des appels , et supposons que toute zone de mémoire (indirectement) accessible à partir de la pile d'appels est active. Cela s'appelle unconservateurGC car les informations de frappe sont perdues: si un entier sur la pile d'appels se présente comme une adresse, il serait suivi, etc.

Dans d'autres cas (plus difficiles), le runtime fournit un ramassage de génération de copie de génération (un exemple typique est le compilateur Ocaml, qui compile Code Ocaml vers code machine utilisant un tel GC). Ensuite, le problème est de trouverprécisémentsur l'appel empile tous les pointeurs, et certains d'entre eux sontdéplacépar le GC. Ensuite, le compilateur génère des métadonnées décrivant les trames de pile d'appels, que le runtime utilise. Ainsi, les conventions d'appelet ABI deviennentspécifiquesà cette implémentation (c'est-à-dire compilateur) et système d'exécution.

Dans certains cas,code machine généré par le compilateur(en fait même fermetures pointant vers lui)est lui-même récupéré. C'est notamment le cas pour SBCL (une bonne implémentation LISP commune) qui génère du code machine pour chaque REPL interaction. Cela nécessite également des métadonnées décrivant le code et les trames d'appel utilisées à l'intérieur.

Le compilateur stocke-t-il une copie d'un programme de récupération de place et la colle-t-il dans chaque exécutable qu'il génère?

Sorte de. Cependant, le système d'exécution peut être une bibliothèque partagée, etc. Parfois (sur Linux et plusieurs autres systèmes POSIX), il peut même s'agir d'un interpréteur de script, par ex. passé à execve (2) avec un Shebang . Ou un ELFE interprète, voir elf (5) et PT_INTERP, etc.

BTW, la plupart des compilateurs de langage avec garbage collection (et leur système d'exécution) sont aujourd'hui logiciels libres . Alors téléchargez le code source et étudiez-le.

23

Il y a déjà de bonnes réponses, mais j'aimerais clarifier certains malentendus derrière cette question.

Il n'y a pas de "langage nativement compilé" en soi. Par exemple, le même code Java Java a été interprété (puis partiellement juste à temps compilé à l'exécution) sur mon ancien téléphone (Java Dalvik) et est (à l'avance) compilé sur mon nouveau téléphone (ART).

La différence entre exécuter du code nativement et interprété est beaucoup moins stricte qu'il n'y paraît. Les deux ont besoin de bibliothèques d'exécution et d'un système d'exploitation pour fonctionner (*). Le code interprété a besoin d'un interprète, mais l'interpréteur n'est qu'une partie de l'exécution. Mais même cela n'est pas strict, car vous pourriez remplacer l'interpréteur par un compilateur (juste à temps). Pour des performances maximales, vous pouvez avoir besoin des deux (desktop Java contient un interpréteur et deux compilateurs).

Peu importe comment exécuter le code, il devrait se comporter de la même manière. L'allocation et la libération de mémoire est une tâche pour le runtime (tout comme l'ouverture de fichiers, le démarrage de threads, etc.). Dans votre langue, vous écrivez simplement new X() ou similaire. La spécification du langage indique ce qui doit arriver et le runtime le fait.

De la mémoire libre est allouée, le constructeur est appelé, etc. Lorsqu'il n'y a pas assez de mémoire, le garbage collector est appelé. Comme vous êtes déjà dans le runtime, qui est un morceau de code natif, l'existence d'un interprète n'a pas d'importance du tout.

Il n'y a vraiment aucun lien direct entre l'interprétation du code et la récupération de place. C'est juste que les langages de bas niveau comme C sont conçus pour la vitesse et le contrôle fin de tout, ce qui ne correspond pas bien à l'idée de code non natif ou à un garbage collector. Il n'y a donc qu'une corrélation.

Cela était très vrai dans le passé, par exemple l'interprète Java était très lent et le garbage collector plutôt inefficace. De nos jours, les choses sont très différentes et parler d'un langage interprété a perdu tout sens.


(*) Au moins quand on parle de code à usage général, en laissant de côté les chargeurs de démarrage et similaires.

6
maaartinus

Les détails varient selon les implémentations, mais il s'agit généralement d'une combinaison des éléments suivants:

  • Une bibliothèque d'exécution qui comprend un GC. Cela gérera l'allocation de mémoire et aura quelques autres points d'entrée, y compris une fonction "GC_now".
  • Le compilateur créera des tables pour le GC afin qu'il sache quels champs dans quels types de données sont des références. Cela sera également fait pour les trames de pile pour chaque fonction afin que le GC puisse tracer à partir de la pile.
  • Si le GC est incrémentiel (l'activité du GC est entrelacée avec le programme) ou simultanée (s'exécute dans un thread séparé), le compilateur inclura également un code objet spécial pour mettre à jour les structures de données du GC lorsque les références sont mises à jour. Les deux ont des problèmes similaires pour la cohérence des données.

Dans le GC incrémentiel et simultané, le code compilé et le GC doivent coopérer pour maintenir certains invariants. Par exemple, dans un collecteur de copie, le GC fonctionne en copiant les données en direct de l'espace A vers l'espace B, en laissant les ordures. Pour le cycle suivant, il retourne A et B et se répète. Ainsi, une règle peut être de garantir que chaque fois que le programme utilisateur essaie de se référer à un objet dans l'espace A, il est détecté et que l'objet est copié immédiatement dans l'espace B, où le programme peut continuer d'y accéder. Une adresse de transfert est laissée dans l'espace A pour indiquer au GC que cela s'est produit afin que toutes les autres références à l'objet soient mises à jour au fur et à mesure de leur traçage. C'est ce qu'on appelle une "barrière de lecture".

Les algorithmes GC ont été étudiés depuis les années 60 et il existe une littérature abondante sur le sujet. Google si vous voulez plus d'informations.

3
Paul Johnson