Comment fonctionne la récupération de place dans les langues compilées de manière native?


79

Après avoir parcouru plusieurs réponses à un dépassement de pile, il est clair que certains langages compilés de manière native ont un garbage collection . Mais je ne vois pas comment cela fonctionnerait.

Je comprends comment la récupération de place pourrait fonctionner avec un langage interprété. Le ramasse-miettes s'exécute simplement à côté de l'interprète et supprime les objets inutilisés et inaccessibles de la mémoire du programme. Ils courent tous les deux ensemble.

Comment cela fonctionnerait-il avec les langages compilés? D'après ce que je comprends, une fois que le compilateur a compilé le code source en code cible - en particulier le code machine natif - c'est fait. Son travail est fini. Alors, comment le programme compilé pourrait-il également être récupéré?

Le compilateur fonctionne-t-il avec le processeur d'une manière ou d'une autre pendant l'exécution du programme pour supprimer les objets "garbage"? Ou le compilateur inclut-il un collecteur de mémoire minimal dans l'exécutable du programme compilé?

Je crois que ma dernière déclaration aurait plus de validité que la première en raison de cet extrait de cette réponse à 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 des CG précis) pour ce code compilé, et aucun ordinateur virtuel n'est nécessaire. En particulier, le compilateur VisualEiffel a généré le code machine x86 natif directement avec le support complet du GC .

La dernière déclaration semble impliquer que le compilateur inclut un programme dans l'exécutable final qui agit comme un ramasse-miettes pendant l'exécution du programme.

La page du site Web du langage D sur la récupération de place, compilée de manière native et dotée d'un récupérateur de place facultatif, semble également indiquer qu'un programme d'arrière-plan s'exécute parallèlement au programme exécutable d'origine pour implémenter la récupération de place.

D est un langage de programmation système prenant en charge la récupération de place. Habituellement, il n'est pas nécessaire de libérer explicitement la mémoire. Allouez-le au besoin, et le ramasse-miettes retournera périodiquement toute la mémoire inutilisée dans le pool de mémoire disponible.

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

Ou suis-je imparfait dans ma pensée? Si tel est le cas, quelles méthodes sont utilisées pour implémenter la récupération de place pour les langages compilés et comment fonctionnent-elles exactement?


1
J'apprécierais que l'électeur proche de cette question puisse énoncer exactement ce qui ne va pas afin que je puisse résoudre le problème?
Christian Dean

6
Si vous acceptez le fait que le catalogue général fait partie d'une bibliothèque requise par une implémentation particulière de langage de programmation, l'essentiel de votre question n'a rien à voir avec le catalogue proprement dit et tout ce qui a trait aux liens statiques versus dynamiques .
Theodoros Chatzigiannakis

7
Vous pouvez considérer que le ramasse-miettes fait partie de la bibliothèque d'exécution qui implémente l'équivalent du langage malloc().
Barmar

9
Le fonctionnement d'un ramasse-miettes dépend des caractéristiques de l' allocateur et non du modèle de compilation . L'allocateur connaît chaque objet alloué. il les a alloués. Désormais, tout ce dont vous avez besoin est de savoir quels objets sont encore en vie et le collecteur peut désallouer tous les objets sauf eux. Rien dans cette description n'a rien à voir avec le modèle de compilation.
Eric Lippert

1
GC est une fonctionnalité de la mémoire dynamique, pas une fonctionnalité de l'interpréteur.
Dmitry Grigoryev

Réponses:


52

La récupération de place dans un langage compilé fonctionne de la même manière que dans un langage interprété. Des langues telles que Go utilisent des récupérateurs de mémoire, même si leur code est généralement compilé à l'avance pour le code machine.

(Suivi) La récupération de place 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. Ensuite, le ramasse-miettes parcourt tous les objets pointés par des objets vivants jusqu'à ce que tout le graphe d'objet actif soit découvert.

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

Il est cependant facile de voir que les langues qui ont des garanties de type fortes (par exemple, si les conversions de pointeur vers différents types de données sont interdites) peuvent effectivement calculer ces cartes au moment de la compilation. Ils stockent simplement une association entre les adresses d'instruction et les cartes de trame de pile et une association entre les types de données et les cartes de présentation d'objet à l'intérieur du fichier binaire. Cette information leur permet ensuite de faire la traversée du graphe d'objet.

Le ramasse-miettes 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()celle qui exécute l'algorithme de collecte si la pression de la mémoire est élevée.


9
Entre les bibliothèques d’utilitaires et la compilation JIT, les lignes entre "compilé en natif" et "s’exécute dans un environnement d’exécution" deviennent de plus en plus floues.
CorsiKa

6
Juste pour ajouter quelques mots sur les langages qui ne viennent pas avec le support du GC: C’est vrai que C et d’autres langages de ce type ne fournissent pas d’informations sur les piles d’appels, mais si vous êtes d'accord avec du code spécifique à la plate-forme code de l’assemblage), il est toujours possible de mettre en œuvre une "collecte conservative des ordures". Le Boehm GC en est un exemple utilisé dans les programmes réels.
Matti Virkkunen

2
@corsiKa Ou plutôt, la ligne est beaucoup plus nette. Nous voyons maintenant que ce sont des concepts différents et non liés, et non des antonymes les uns des autres.
Kroltan

4
Une complexité supplémentaire dont vous devez tenir compte dans les exécutions compilées / interprétées est liée à cette phrase de votre réponse: "(Le traçage) commence par traiter les piles d'appels de tous les threads en cours d'exécution." Mon expérience de la mise en œuvre de GC dans un environnement compilé est que le traçage des piles ne suffit pas. Le point de départ consiste généralement à suspendre les threads assez longtemps pour pouvoir effectuer un suivi dans leurs registres , car ils peuvent avoir des références dans ces registres qui n'ont pas encore été stockées dans la pile. Pour un interprète, ce n'est généralement pas ...
Jules

... un problème, car l'environnement peut faire en sorte que la GC se déroule à des "points sûrs" où l'interprète sait que toutes les données sont stockées en toute sécurité dans les piles interprétées.
Jules

123

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

Cela semble peu élégant et bizarre, mais oui. Le compilateur a une bibliothèque d’utilitaires complète, contenant beaucoup 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 habituellement.


51
@ChristianDean Notez que même C a une bibliothèque d'exécution. Bien qu'il ne dispose pas de GC, il effectue toujours la gestion de la mémoire via cette bibliothèque d'exécution: malloc()et free()n'est pas intégré à la langue, ne fait pas partie du système d'exploitation, mais fait partie de cette bibliothèque. C ++ est aussi parfois compilé avec une bibliothèque de récupération de place, même si le langage n'a pas été conçu avec GC.
am le

18
C ++ contient également une bibliothèque d'exécution qui fait des choses comme make dynamic_castet que les exceptions fonctionnent, même si vous n'ajoutez pas de GC.
Sebastian Redl

23
La bibliothèque d'exécution n'est pas nécessairement copiée dans chaque exécutable (appelée liaison statique); elle peut uniquement être référencée (chemin d'accès au binaire contenant la bibliothèque) et accessible au moment de l'exécution: il s'agit d'une liaison dynamique.
mouviciel

16
Le compilateur n'est pas non plus obligé de sauter directement dans le point d'entrée de votre programme sans que rien ne se passe. Je suppose que chaque compilateur insère en réalité une série de codes d'initialisation spécifiques à la plate-forme avant d'appeler main(), et il est parfaitement légal de lancer un thread GC dans ce code. (En supposant que le GC ne soit pas implémenté dans les appels d'allocation de mémoire.) Au moment de l'exécution, le GC n'a vraiment besoin que de savoir quelles parties d'un objet sont des pointeurs ou des références d'objet, et le compilateur doit émettre le code pour traduire une référence d'objet en pointeur. si le CPG déplace des objets.
Millimoose

15
@ millimoose: oui. Par exemple, le GCC, ce morceau de code est crt0.o( ce qui signifie « C R un T ime, les bases »), qui obtient lié à tous les programmes (ou au moins tous les programmes qui ne sont pas autoportants ).
Jörg W Mittag

58

Ou bien le compilateur inclut-il un collecteur de mémoire minimal dans le code du programme compilé?

C'est une façon étrange de dire «le compilateur lie le programme à une bibliothèque qui effectue le ramassage des ordures». Mais oui, c'est ce qui se passe.

Cela n'a rien de spécial: les compilateurs lient généralement des tonnes de bibliothèques aux programmes qu'ils compilent; autrement, les programmes compilés ne pourraient pas faire grand chose sans ré-implémenter beaucoup de choses: 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 effectuent beaucoup de tâches en arrière-plan sans API publique, au-delà de GC. Considérez ces trois exemples:

  1. Propagation d'exception et dérouleur de pile / appel de destructeur.
  2. Allocation dynamique de mémoire (qui n'appelle généralement pas simplement une fonction, comme en C, même en l'absence de récupération de place).
  3. Suivi des informations de type dynamique (pour les conversions, etc.).

Ainsi, une bibliothèque de récupération de place n'a rien de spécial, et a priori n'a rien à voir avec le fait qu'un programme a été compilé à l'avance.


cela ne semble pas offrir quoi que ce soit de substantiel sur les points exprimés et expliqués dans la réponse la plus élevée postée 3 heures auparavant
Gnat

11
Je pensais que c'était utile / nécessaire parce que la réponse principale n'était pas assez forte: elle mentionne des faits similaires, mais elle ne précise pas que le fait de sélectionner le ramassage des ordures est une distinction complètement artificielle. Fondamentalement, l'hypothèse d'OP est erronée et la réponse principale ne le mentionne pas. Le mien le fait (en évitant le terme plutôt brutal «imparfait»).
Konrad Rudolph

Ce n'est pas si spécial, mais je dirais que c'est un peu spécial, car les gens pensent généralement que les bibliothèques sont quelque chose qu'ils appellent explicitement à partir de leur code; plutôt qu'une implémentation de la sémantique fondamentale des langues. Je pense que la mauvaise hypothèse de l'OP ici est plutôt qu'un compilateur ne fait que traduire le code avec une méthode plus ou moins simple, plutôt que de l'instruire avec des appels de bibliothèque que l'auteur n'a pas spécifiés.
Millimoose

7
Les bibliothèques @millimoose Runtime fonctionnent en coulisse de multiples façons, sans interaction explicite de l'utilisateur. Envisagez la propagation des exceptions et le dénouement / l'appel de destructeurs de pile. Considérez l’allocation dynamique de mémoire (qui n’appelle généralement pas une fonction, comme en C, même s’il n’ya pas de garbage collection). Envisagez le traitement des informations de type dynamique (pour les conversions, etc.). Le GC n'est donc pas unique.
Konrad Rudolph

3
Oui, j'avoue que j'avais formulé cela étrangement. C'était simplement parce que j'étais sceptique vis-à-vis du compilateur. Mais maintenant que j'y pense, cela a plus de sens. Le compilateur pourrait simplement lier un ramasse-miettes comme n'importe quelle autre partie de la bibliothèque standard. Je crois que ma confusion provient en partie du fait de considérer un éboueur comme une partie seulement de la mise en œuvre d’un interprète et non comme un programme à part entière.
Christian Dean

23

Comment cela fonctionnerait-il avec les langages compilés?

Votre formulation est fausse. Un langage de programmation est une spécification écrite dans un rapport technique (pour un bon exemple, voir R5RS ). En fait, vous faites référence à une implémentation de langage spécifique (qui est un logiciel).

(certains langages de programmation ont de mauvaises spécifications, voire des références manquantes, ou se conforment simplement à un exemple d'implémentation; néanmoins, un langage de programmation définit un comportement - par exemple, il a une syntaxe et une sémantique -, ce n'est pas un logiciel, mais pourrait l'être implémenté par certains logiciels; de nombreux langages de programmation ont plusieurs implémentations, en particulier, "compilé" est un adjectif qui s'applique aux implémentations - même si certains langages de programmation sont plus faciles à implémenter par des interprètes que par des compilateurs.)

D'après ce que j'ai compris, une fois que le compilateur a compilé le code source en code cible - en particulier le code machine natif - c'est fait. Son travail est fini.

Notez que les interprètes et les compilateurs ont une signification 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 livre de dragon et réfléchissez au bytecode , à la compilation JIT , à l’ émission dynamique de code C compilé dans un "plugin" puis à dlopen (3), soumis au même processus (et sur les machines actuelles, cela est suffisamment rapide pour être compatible avec un REPL interactif , voir ceci )


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

Le système d'exécution de l'implémentation du langage compilé contient le ramasse-miettes et le compilateur génère un code adapté à ce système d'exécution particulier. En particulier, les primitives d’allocation (compilées en code machine) 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 un 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 collecte des ordures, en particulier Boehm GC , MPS Ravenbrook , ou même mon (unmaintained) Qish . Et coder un GC simple n’est pas très difficile (cependant, le déboguer est plus difficile et coder un GC concurrentiel est difficile ).

Dans certains cas, le compilateur utiliserait un GC conservateur (comme Boehm GC ). Ensuite, il n'y a pas grand chose à coder. Le CPG conservateur (lorsque le compilateur appelle sa routine d’allocation ou la routine entière du GC) analyse parfois la pile d’appels entière et suppose que toute zone mémoire (indirectement) accessible depuis la pile d’appels est active. Ceci est appelé un GC conservateur car les informations de frappe sont perdues: si un entier de la pile d'appels ressemble à une adresse, il sera suivi, etc.

Dans d'autres cas (plus difficiles), le moteur d'exécution fournit un garbage collection pour la copie générationnelle (un exemple typique est le compilateur Ocaml, qui compile le code Ocaml en code machine à l'aide d'un tel GC). Ensuite, le problème est de trouver précisément sur la pile d’appels tous les pointeurs, et certains d’entre eux sont déplacés par le GC. Ensuite, le compilateur génère des méta-données décrivant les cadres de pile d'appels, que le moteur d'exécution utilise. Ainsi, les conventions d’appel et ABI deviennent spécifiques à cette implémentation (c’est-à-dire un compilateur) et à ce système d’exécution.

Dans certains cas, le code machine généré par le compilateur (même les fermetures pointant vers lui) est lui-même récupéré . C'est notamment le cas de SBCL (une bonne implémentation de Common Lisp) qui génère un code machine pour chaque interaction REPL . Cela nécessite également des méta-données décrivant le code et les cadres d'appel utilisés à l'intérieur.

Le compilateur stocke-t-il une copie d'un programme de récupération de place et la colle-t-elle 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 (sous Linux et plusieurs autres systèmes POSIX), il peut même s'agir d'un interpréteur de script, par exemple transmis à execve (2) avec un shebang . Ou un interprète ELF , voir elf (5) et PT_INTERP, etc.

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


5
Vous voulez dire qu'il existe de nombreuses implémentations de langage de programmation sans spécification explicite. Oui, je suis d'accord avec ça. Mais ce que je veux dire, c'est qu'un langage de programmation n'est pas un logiciel (comme un compilateur ou un interprète). C'est quelque chose qui a une syntaxe et une sémantique (les deux étant peut-être mal définis).
Basile Starynkevitch

4
@ KonradRudolph: Cela dépend entièrement de votre définition de "formel" et de "spécification" :-D Il existe la spécification du langage de programmation Ruby ISO / IEC 30170: 2012 , qui spécifie un petit sous-ensemble de l'intersection de Ruby 1.8 et 1.9. Il y a la suite Ruby Spec , un ensemble d'exemples de cas limites qui servent de sorte de "spécifications exécutables". Ensuite, le langage de programmation Ruby de David Flanagan et Yukihiro Matsumoto .
Jörg W Mittag

4
Aussi, la documentation Ruby . Discussions de problèmes sur le Ruby Issue Tracker . Discussions sur les listes de diffusion ruby-core (anglais) et ruby-dev (japonais). Les attentes de bon sens de la communauté (par exemple, le Array#[]pire cas Hash#[]0 (1), le pire cas amorti 0 (1)). Et le dernier mais non le moindre: le cerveau de Matz.
Jörg W Mittag Le

6
@ KonradRudolph: Le problème est le suivant: même un langage sans spécification formelle et une seule inplémentation peut toujours être séparé en "langage" (règles abstraites et restrictions) et "mise en oeuvre" (programmes de traitement de code conformes à ces règles et règles). restrictions). Et la mise en oeuvre donne toujours lieu à une spécification, même si elle est triviale, à savoir: "quel que soit le code utilisé, c’est la spécification". C’est ainsi que les spécifications ISO, RubySpec et RDocs ont été écrits, après tout: en jouant avec l’IRM de synthèse et / ou de reverse engineering.
Jörg W Mittag Le

1
Je suis content que vous ayez parlé du éboueur de Bohem. Je recommanderais à l’opérateur de l’étudier car c’est un excellent exemple de la simplicité de la collecte des ordures, même «intégrée» à un compilateur existant.
Cort Ammon

6

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

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

La différence entre exécuter du code en mode natif et interprété est beaucoup moins stricte qu'il n'y parait. 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ète n'est qu'une partie du moteur d'exécution. Mais même cela n’est pas strict, car vous pourriez remplacer l’interprète par un compilateur (juste à temps). Pour obtenir des performances optimales, vous pouvez choisir les deux (le runtime Java sur le bureau 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'affectation et la libération de mémoire sont des tâches qui incombent au moteur d'exécution (tout comme l'ouverture de fichiers, le démarrage de threads, etc.). Dans votre langue, vous écrivez new X()ou vous ressemblez. La spécification de langue indique ce qui doit arriver et le moteur d'exécution le fait.

Une partie de la mémoire disponible est allouée, le constructeur est appelé, etc. S'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.

Il n'y a vraiment pas de lien direct entre l'interprétation du code et la récupération de place. C'est juste que les langages de bas niveau tels que C sont conçus pour la vitesse et le contrôle fin de tout, ce qui ne cadre pas bien avec l'idée d'un code non natif ou d'un ramasse-miettes. Donc, il n'y a qu'une corrélation.

C'était très vrai dans les temps anciens, où par exemple, l'interpréteur Java était très lent et le ramasse-miettes 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 autres.


Ocaml et SBCL sont tous deux des compilateurs natifs. Donc , il y a des implémentations « langage compilé en mode natif ».
Basile Starynkevitch

@BasileStarynkevitch WAT? Quel est le lien entre ma réponse et les compilateurs moins connus? SBCL en tant que compilateur d’un langage interprété à l’origine ne constitue-t-il pas un argument en faveur de mon affirmation selon laquelle la distinction n’a aucun sens?
Maaartinus

Common Lisp (ou tout autre langage) n'est pas interprété ni compilé. C'est un langage de programmation (une spécification). Son implémentation peut être un compilateur, ou un interpréteur, ou quelque chose entre les deux (par exemple, un interpréteur de code-octet). SBCL est une implémentation interactive compilée de Common Lisp. Ocaml est également un langage de programmation (avec à la fois un interpréteur bytecode et un compilateur natif comme implémentations).
Basile Starynkevitch

@ BasileStarynkevitch C'est ce que je prétends. 1. Il n’existe pas de langage interprété ou compilé (bien que C soit rarement interprété et que LISP ait été rarement compilé, mais cela n’a pas vraiment d’importance). 2. Il existe des implémentations interprétées, compilées et mixtes pour la plupart des langages bien connus et aucun langage n'exclut la compilation ou l'interprétation.
Maaartinus

6
Je pense que votre argument a beaucoup de sens. Le point clé de grok est que vous exécutez toujours un "programme natif", ou "jamais", comme vous le souhaitez. Aucun exe sur Windows n'est en soi exécutable; il a besoin d’un chargeur et d’autres fonctionnalités du système d’exploitation pour commencer et est en partie partiellement «interprété». Cela devient plus évident avec les exécutables .net. java myprogest autant ou aussi peu natif que grep myname /etc/passwdor ld.so myprog: c'est un exécutable (peu importe ce que cela veut dire) qui prend un argument et effectue des opérations avec les données.
Peter - Réintégrer Monica le

3

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 inclut un GC. Ceci gérera l'allocation de mémoire et aura d'autres points d'entrée, y compris une fonction "GC_now".
  • Le compilateur créera des tables pour le CPG afin qu'il sache quels champs dans lesquels les types de données sont des références. Cette opération est également effectuée pour les trames de pile de chaque fonction afin que le CPG puisse effectuer le suivi à partir de la pile.
  • Si le CPG est incrémental (l'activité du CPG est entrelacée avec le programme) ou simultané (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 les GC incrémentiels et simultanés, le code compilé et le GC doivent coopérer pour gérer certains invariants. Par exemple, dans un collecteur de copie, le CPG copie les données en temps réel de l’espace A à l’espace B, en laissant derrière elles les déchets. Pour le cycle suivant, il retourne A et B et se répète. Une règle peut donc être de s'assurer que chaque fois que le programme utilisateur tente de se référer à un objet dans l'espace A, cela est détecté et l'objet est immédiatement copié dans l'espace B, où le programme peut continuer à y accéder. Une adresse de transfert est laissée dans l'espace A pour indiquer au GC que cela s'est produit, de sorte que toute autre référence à l'objet soit mise à jour au fur et à mesure de son traçage. Ceci est connu comme 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.

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.