4 points par GN⁺ 2024-03-31 | 1 commentaires | Partager sur WhatsApp
  • Même dans du code où les performances et la concurrence sont cruciales, comme les noyaux et les pilotes, la libération de mémoire ne se résume pas à un simple appel à free() ; des techniques de partage sans verrou peuvent devenir nécessaires
  • RCU (Read, Copy, Update) consiste à copier des données souvent lues et rarement modifiées, puis à remplacer atomiquement le pointeur, afin de ne pas bloquer le chemin de lecture
  • Si l’on delete immédiatement l’ancienne version, un use-after-free peut se produire dans un thread qui est encore en train de la lire ; RCU suit donc les sections de lecture et retarde la libération jusqu’à un moment sûr
  • RCU est utilisé des dizaines de milliers de fois dans Linux, existe dans la bibliothèque C++ Folly et dans crossbeam-epoch pour Rust ; dans la mesure où les objets sont nettoyés plus tard selon qu’ils sont encore utilisés ou non, il prend une forme proche d’un GC
  • La dichotomie selon laquelle la gestion manuelle de la mémoire serait toujours plus rapide et plus prévisible est fragile : free(), le comptage de références et le comportement mémoire de l’OS ont eux aussi des coûts et des incertitudes, si bien qu’un GC moderne peut également être un outil de programmation système

Pourquoi le code noyau utilise des techniques comme RCU

  • Parmi les programmes exécutés au quotidien, les systèmes d’exploitation font partie de ceux dont la sensibilité aux performances est la plus élevée
  • Si l’OS devient plus rapide, l’utilisateur peut effectuer davantage de calculs ; les développeurs de noyaux et de pilotes consacrent donc beaucoup d’efforts à l’optimisation du code
  • Un système d’exploitation doit gérer non seulement les processus et threads de l’espace utilisateur, mais aussi plusieurs threads du noyau lui-même, ainsi que les gestionnaires d’interruptions matérielles
  • Comme une latence accrue fait perdre du temps à l’utilisateur, le code noyau a vu apparaître diverses techniques permettant de partager des données entre threads sans verrou

Fonctionnement de base de RCU

  • RCU (Read, Copy, Update) est une approche adaptée aux données lues très fréquemment mais écrites rarement
    • Un exemple serait l’ensemble des périphériques USB actuellement connectés : il change peu, mais peut changer
    • La modification doit se produire de manière atomique et ne doit pas bloquer les lecteurs déjà en train de lire
  • L’écrivain met à jour l’état partagé dans l’ordre suivant
    • Lire les données existantes depuis le pointeur
    • Copier les données existantes et appliquer les modifications nécessaires pour créer une nouvelle version
    • Mettre à jour atomiquement le pointeur pour qu’il pointe vers la nouvelle version
  • Les lecteurs ne font que lire le pointeur partagé ; le chemin de lecture reste donc simple et fonctionne sans attente
  • Cette approche est facile à utiliser et wait-free, mais si les anciennes versions ne sont pas nettoyées, elle provoque une fuite mémoire

Les anciennes versions ne peuvent pas être libérées immédiatement

  • Si l’on remplace le pointeur par la nouvelle version puis que l’on delete immédiatement l’ancienne, on crée un risque de use-after-free
  • Comme tout fonctionne sans verrou, l’écrivain ne peut pas savoir s’il reste des lecteurs en train de lire l’ancienne version
  • Les lecteurs peuvent délimiter leur section critique côté lecture avec rcu_read_lock() et rcu_read_unlock()
    • Les lecteurs ne sont toujours pas bloqués
    • L’écrivain ne supprime pas les anciennes données tant que ces lecteurs ne sont pas sortis
  • rcu_synchronize() n’a pas besoin d’attendre que tous les lecteurs aient disparu, mais seulement que les anciens lecteurs susceptibles de voir l’ancienne version aient terminé
    • Les lecteurs qui ont vu le nouveau pointeur utilisent la nouvelle version et n’ont donc aucun lien avec la durée de vie de l’ancienne

Libération différée et forme de GC

  • Même si l’écrivain n’attend pas dans la fonction de mise à jour, le code reste correct tant que les anciennes données sont libérées en toute sécurité à un moment donné
  • Une approche comme rcu_defer(old) permet de libérer old à n’importe quel moment après que les lecteurs actuels sont sortis de leur section critique
  • Une forme où un thread dédié nettoie périodiquement les anciennes versions non référencées ressemble à un GC générationnel
  • RCU n’est pas une expérience de pensée, mais une technique réellement très répandue
    • Linux utilise RCU des dizaines de milliers de fois
    • RCU est fourni dans la bibliothèque C++ Folly de Facebook
    • En Rust, on l’utilise sous le nom crossbeam-epoch, et il sert de base à une bibliothèque de concurrence populaire
  • Plus que le débat de classification visant à savoir si RCU est un « vrai GC », l’important est que sa structure, où la mémoire est nettoyée plus tard selon qu’elle est encore utilisée ou non, est la même que celle d’un GC

Les coûts cachés de la libération manuelle

  • L’idée reçue selon laquelle un GC serait intrinsèquement moins efficace que la gestion manuelle de la mémoire s’affaiblit dès que l’on regarde les détails d’implémentation
  • free() n’est pas gratuit

    • Un allocateur mémoire généraliste doit gérer un état global interne : pages obtenues auprès du noyau, découpage en buckets par taille, buckets en cours d’utilisation, etc.
    • Plusieurs threads peuvent entrer en contention en essayant de verrouiller l’état de l’allocateur
    • Même avec des pools thread-local comme dans jemalloc, du code supplémentaire est nécessaire pour les synchroniser
  • RAII et les lifetimes ne suppriment pas non plus le coût de l’allocateur

    • Les lifetimes de Rust ou le RAII de C++ aident à automatiser la libération mémoire et à garantir la correction, mais ils ne suppriment pas la complexité de la structure interne de l’allocateur
    • Dans de nombreux scénarios, il faut revenir à shared_ptr ou à Arc
    • Cela nécessite des métadonnées supplémentaires sous forme de compteurs de références, et ces valeurs peuvent circuler entre cœurs et caches en générant un coût
    • Des cycles dans le graphe de vivacité peuvent également provoquer des fuites
  • Un GC peut aussi offrir des optimisations

    • Un GC générationnel avec déplacement recompresse périodiquement le tas
    • L’allocation devient proche d’une simple incrémentation de pointeur, ce qui permet un débit élevé
    • La localité des allocations séquentielles s’améliore, ce qui aide aussi les performances du cache

L’illusion du contrôle sur la gestion mémoire

  • Beaucoup de développeurs opposés au GC construisent des systèmes soft real-time
    • Ils veulent un fonctionnement aussi rapide que possible, comme les FPS d’un jeu vidéo ou les performances de compression d’un codec de streaming
    • Mais il ne s’agit pas d’exigences de latence dure où un délai occasionnel d’une milliseconde casserait le système ou tuerait quelqu’un
  • L’idée que le programmeur peut décider quand la gestion mémoire se produit n’est pas simple
    • Le système d’exploitation abstrait les interactions avec le matériel
    • Par défaut, Linux ne fait presque rien lors d’une demande de mémoire et peut ne fournir effectivement la mémoire que lorsqu’on tente de l’utiliser
    • Avec madvise(), les E/S mappées en mémoire et le cache du système de fichiers, il n’existe pas de réponse simple à la question « qu’est-ce qui a été alloué, et quand ? »
    • Les mauvais jours, un simple accès à un pointeur peut même déclencher des E/S disque
  • La croyance selon laquelle le programmeur sait toujours quel est le bon moment pour s’arrêter afin de gérer la mémoire est elle aussi limitée
    • Il existe des cas clairs, comme l’écran de chargement d’un jeu vidéo
    • Dans beaucoup de logiciels, la seule réponse est : quand on n’est pas occupé par une tâche plus importante
    • Un morceau de code individuel utilisant shared_ptr ou Arc ne peut pas savoir à l’avance s’il deviendra le dernier propriétaire et devra se charger du nettoyage
  • L’idée qu’un appel à free() rend immédiatement la mémoire à l’OS n’est pas toujours correcte non plus
    • La mémoire est allouée par l’OS au niveau des pages
    • Les allocateurs conservent souvent les pages et les réutilisent jusqu’à la fin du programme
    • L’OS peut aussi récupérer des pages via le swap

Pourquoi on peut voir le GC comme un outil de programmation système

  • Tous les logiciels ne bénéficient pas du GC
  • Pourtant, même à une époque proche de 2024, les discussions sur le GC parmi les programmeurs système restent facilement noyées dans une fausse dichotomie et dans la peur, l’incertitude et le doute
  • L’idée que les langages avec GC seraient « manifestement » plus lents que les langages à gestion mémoire manuelle n’est pas un fait ; elle relève plutôt de l’idéologie
  • Même dans des équipes qui construisent des systèmes où des vies sont en jeu, il existe des exemples de latences sous la microseconde fournies avec un langage à GC qui alloue sur presque chaque ligne
  • Si une partie du système doit impérativement s’exécuter en n cycles d’horloge, cette partie précise peut être isolée dans du code sans GC ou dans du matériel
  • Le GC n’est pas une solution miracle, mais c’est l’un des outils de la boîte à outils que l’on peut utiliser sans crainte

1 commentaires

 
GN⁺ 2024-03-31
Commentaires Hacker News
  • Parmi les techniques modernes prometteuses de garbage collection parallèle, MPL ou MaPLe et sa nouvelle approche Automatic Management of Parallelism méritent le détour.
    Le cœur des travaux récompensés par le distinguished paper award de POPL 2024 et l’ACM SIGPLAN dissertation award 2023 tient en deux points : a) une garbage collection parallèle prouvablement efficace fondée sur le disentanglement, b) un contrôle automatique de la granularité prouvablement efficace.
    [1] MaPLe (MPL) : https://github.com/MPLLang/mpl
    [2] Automatic Parallelism Management : https://dl.acm.org/doi/10.1145/3632880

    • Standard ML et sa communauté ont apporté des contributions assez impressionnantes à la littérature sur la gestion mémoire.
      Il y a l’article lié, mais aussi MLKit, qui a été l’un des premiers utilisateurs et pionniers de la gestion mémoire par régions.
    • Je me demande ce que signifie ici « prouvablement efficace ».
    • Je me demande comment cette approche se compare aux travaux récents menés dans OCaml pour prendre en charge le parallélisme multicœur.
    • Question à celles et ceux qui s’y connaissent mieux : dans quelle mesure cela pourrait-il s’appliquer à d’autres langages ?
      Par exemple, une telle approche pourrait-elle rendre la garbage collection de Go nettement plus rapide, ou se heurterait-elle à des problèmes liés à la conception existante du langage ?
  • Les cas d’usage de RCU sont convaincants, mais mon expérience du ramasse-miettes dans d’autres situations n’a pas été bonne
    Je lis cet article moins comme une affirmation selon laquelle les durées de vie statiques sont généralement préférables aux durées de vie dynamiques, que comme l’idée que des solutions de gestion mémoire sur mesure peuvent offrir les meilleures performances
    Je ne pars pas du principe que free() rend la mémoire au système d’exploitation, mais plutôt qu’il la rend à l’allocateur. C’est bien mieux que de la rendre au système d’exploitation, et les appels système sont lents. Cela dit, comme mimalloc, on peut aussi faire en sorte que la mémoire libérée ne soit pas immédiatement disponible pour le prochain malloc, mais seulement périodiquement
    Si l’on alloue 800 octets puis qu’on les free immédiatement un million de fois et qu’on compte le nombre de pointeurs uniques, glibc malloc donne 1, jemalloc 1, mimalloc 4, et le ramasse-miettes de Julia 62767
    62767, soit environ 48 Mio, ce n’est pas catastrophique, mais cela expulse quand même le cache L3 de ma machine. Avec le ramasse-miettes, il est presque garanti que les nouvelles allocations viennent de la RAM plutôt que du cache, ce qui ruine les performances du code qui alloue beaucoup. Ce qui compte, ce n’est pas seulement la vitesse de la gestion mémoire elle-même, mais aussi la vitesse à laquelle on peut travailler avec la mémoire qu’elle a fournie
    J’ai publié un benchmark qui le montre dans Julia : https://discourse.julialang.org/t/blog-post-rust-vs-julia-in...
    malloc/free donne à la mémoire de travail réelle une chance de rester chaude dans le cache si elle est suffisamment petite. Des allocateurs comme mimalloc sont aussi conçus, à la manière d’un ramasse-miettes compactant, pour placer les allocations consécutives proches les unes des autres, et les 4 pointeurs uniques que j’ai vus avec mimalloc étaient espacés de 896 octets
    Si j’avais eu davantage d’expérience avec les ramasse-miettes compactants, je serais peut-être moins cynique, mais je considère le ramasse-miettes comme une solution bien plus complexe pour assurer une gestion mémoire sûre que quelque chose comme le vérificateur d’emprunts de Rust. Comme cette complexité est reportée sur les développeurs du compilateur et du runtime, c’est généralement acceptable pour l’utilisateur, et c’est un compromis recevable quand on écrit du code peu sensible aux performances. Le RAII avec des durées de vie statiques est aussi un compromis raisonnable pour du code qui ne nécessite pas une approche plus personnalisée, et les exemples de l’article relèvent clairement de cas où une solution sur mesure est nécessaire

    • Il ne suffit pas de dire que la localité du cache est mauvaise simplement parce que le ramasse-miettes a fourni davantage de pointeurs
      Dans les programmes de longue durée, un ramasse-miettes compactant utilise presque toujours mieux le cache que malloc, car la fragmentation du tas gaspille des entrées du cache TLB et de l’espace libre entre les objets. L’allocateur par bump allocation d’un ramasse-miettes compactant donne un nouveau pointeur à chaque allocation puisque free ne récupère pas la mémoire, mais ces allocations sont séquentielles et, si le programme consomme progressivement le tas tout en ne touchant que les objets les plus récents, elles restent tout de même dans le cache. Il est extrêmement difficile de benchmarker les effets en cascade des allocateurs et des ramasse-miettes, et je regarde presque tous ces benchmarks synthétiques avec scepticisme
    • L’article explique pourquoi cette approche fonctionne bien dans le contexte de RCU, pourquoi elle est généralement moins bonne, puis l’écarte et l’ignore
      Ce qui inquiète les gens, c’est que déterminer si la mémoire n’est plus utilisée est moins efficace et moins déterministe que de dire directement à l’allocateur qu’on a fini d’utiliser la ressource. Je n’ai jamais vu quelqu’un s’inquiéter du simple fait de retarder la libération
      Parcourir tout l’ensemble vivant est rare, et trente ans d’améliorations des algorithmes de ramasse-miettes les ont amenés presque au niveau de la conscience, mais cette phrase ignore, volontairement ou non, le point que les gens critiquent réellement. Quand un service a un problème de ramasse-miettes, on a l’impression de devoir appeler un chaman pour tout régler dans tous les sens en espérant renvoyer les esprits en colère dans le monde des ombres
      Si l’on marque les déchets et qu’on reçoit une notification quand ils ne sont plus utilisés, tout ce processus disparaît. L’allocation mémoire peut être très rapide avec un ramasse-miettes, mais pour comparer équitablement, il faut aussi amortir et inclure les coûts du marquage et de la compaction
      Un autre gros problème est que, pour obtenir les mêmes performances, le ramasse-miettes demande généralement beaucoup plus de mémoire que la gestion mémoire manuelle. Il faut aussi du CPU supplémentaire pour revérifier à répétition s’il reste des références, et accepter des copies mémoire supplémentaires pour la compaction optimiste
      Enfin, l’article critique le fait qu’il faille, quand les durées de vie sont floues, une gestion mémoire manuelle comme Arc/Rc de Rust, mais il ignore que, dans les langages à ramasse-miettes, on ne peut pas non plus compter sur l’appel garanti des finalizers, si bien qu’on construit en pratique exactement la même infrastructure pour fermer les ressources externes
      Ce débat a déjà été rejoué suffisamment de fois ces 20 ou 30 dernières années, et cet article ne semble rien apporter de nouveau, à part balayer des inquiétudes légitimes sur le ramasse-miettes comme si c’étaient des mèmes. Les mèmes sont amusants, donc pourquoi pas, mais la bonne réponse est qu’il n’existe pas de bonne réponse générale. Il faut utiliser l’outil qui convient pour satisfaire les contraintes de conception du système
    • Je ne comprends pas bien cet argument. Avec un ramasse-miettes générationnel, la génération 0 a de bonnes chances d’être dans le cache, et c’est précisément là que se produisent la plupart des créations et destructions
      Les autres allocations, qui vivent plus longtemps, sont par définition moins faciles à contrôler du point de vue du cache. La localité est l’un des grands avantages du ramasse-miettes, et le seul problème que je connaisse est le marquage/balayage avec arrêt du monde. Je sais que les ramasse-miettes modernes ont des threads en arrière-plan, mais à ma connaissance il se produit quand même des événements d’arrêt du monde
    • Il est vrai que free() rend la mémoire à l’allocateur, mais gérer la fragmentation mémoire dans un serveur de longue durée n’a rien d’agréable
      Surtout la fragmentation interne des pages gérées par un allocateur slab ; ce n’est pas un problème courant, mais il est difficile à traiter
    • Si l’utilisation du cache est une préoccupation aussi importante, l’allocation par arène fonctionne aussi bien avec un ramasse-miettes qu’avec l’allocation mémoire manuelle
      Heureusement, il n’y a pas beaucoup de domaines où le ramasse-miettes doive rivaliser avec des exemples aussi commodément arrangés
  • Sauf cas particulier où toute la mémoire peut facilement être gérée via des arènes, un bon garbage collector par traçage a depuis longtemps dépassé la gestion manuelle de la mémoire en débit, et, plus récemment, son impact sur la latence est devenu suffisamment acceptable pour la grande majorité des applications
    ZGC d’OpenJDK a des temps de pause typiques de l’ordre de quelques dizaines à quelques centaines de microsecondes, et, avec un taux d’allocation raisonnable, le pire cas dépasse rarement 1 ms ; c’est du même ordre que les pauses provoquées par le système d’exploitation
    Le vrai compromis important, c’est seulement l’usage mémoire. En dehors de niches particulières — quand les arènes conviennent très bien à tout et que la latence maximale se situe dans les quelques microsecondes — la question essentielle est unique : mon application tourne-t-elle dans un environnement contraint en mémoire, ou vaut-il la peine de sacrifier autre chose pour réduire l’usage de la RAM ?

    • D’après mon expérience, c’est l’inverse. Les durées de vie individuelles par objet sont un cas particulier rare, et la plupart du code réel contient beaucoup d’objets liés qui ont des durées de vie identiques ou très proches
      Dans ce genre de code, suivre la durée de vie de chaque objet individuellement est excessif. Au final, la gestion mémoire est une affaire de durée de vie, et il vaut toujours mieux avoir peu de durées de vie distinctes que beaucoup. Parce que cela fait moins de travail, que ce soit manuel ou automatique
      Ne pas avoir à réfléchir à la durée de vie des objets est extrêmement pratique, et c’est pour cela que les langages à garbage collection ont réussi, malgré la complexité interne considérable des bons garbage collectors
    • Les temps de pause ont été en partie résolus, mais la consommation CPU du garbage collection reste encore assez élevée
      On continue aussi de subir des latences de queue difficiles à prévoir et l’effet de nombreux cas particuliers
    • Au travail, nous benchmarkons ZGC et Shenandoah, et les temps de pause p100 sont généralement sous les 500 µs
      ZGC semble faire moins de pauses que Shenandoah, et donc accomplir davantage de travail par pause, ce qui donne des performances un peu meilleures
      Il nous reste encore à tester en production, mais jusqu’ici, avec ZGC — et le ZGC générationnel depuis Java 21 — les pauses de garbage collection semblent être un problème largement résolu
    • Le critère selon lequel toute la mémoire devrait pouvoir être facilement gérée via des arènes me paraît injuste. Si la plupart des objets peuvent être facilement alloués dans des arènes, cela suffit déjà à éliminer l’essentiel du besoin de garbage collection
      Dans la perspective de Jai, les allocations mémoire peuvent être classées en quatre catégories, par ordre de fréquence : 1) celles à durée de vie extrêmement courte, qui peuvent aller sur la pile de la fonction, 2) celles à durée de vie courte et bien définie, qui peuvent aller dans une arène mémoire par frame/requête, 3) celles à durée de vie longue avec un propriétaire bien défini, qui peuvent être gérées par un pool dédié à un sous-système, 4) celles à durée de vie longue et au propriétaire flou, qui nécessitent une gestion dynamique de la mémoire
      Pour affirmer que le garbage collection par traçage surpasse généralement la gestion manuelle de la mémoire, il faut le comparer non pas à un système qui appelle malloc/free un peu partout, mais à un système écrit avec cette perspective en tête. La comparaison serait peut-être plus équitable avec les pratiques modernes en C++/Rust
      Je suis d’accord pour dire que, dans la plupart des systèmes, s’appuyer sur un garbage collector par traçage a de fortes chances d’être beaucoup plus pratique, mais c’est une affirmation totalement différente
    • Il faut des preuves
  • L’article motive d’abord RCU, puis fait demi-tour et se met à défendre de façon générale le garbage collection généraliste
    Ce n’est pas tout à fait un cheval de Troie, mais cela ressemble à un virage assez brutal

    • Je n’appellerais pas RCU du garbage collection. À aucun moment un objet n’est dans un état de déchet
      Un objet est dans l’un de trois états et passe de l’un à l’autre aussi vite que possible : active, obsolete but alive for old readers, deallocated
      Selon la manière dont le code est écrit, il semble possible de réutiliser en toute sécurité les objets « obsolete-but-alive » pour de nouvelles allocations, mais je n’en ai pas analysé complètement les performances
      Comme souvent dans les discussions sur le garbage collection, le moment où il faut « se rabattre » sur shared_ptr/Arc est très flou. En pratique, éviter le comptage de références — c’est-à-dire prouver qu’on a déjà la propriété, ou éviter complètement l’indirection — est au cœur de tout système sérieux fondé sur le comptage de références. Ne rien faire est évidemment meilleur que le « faire quelque chose un jour » du garbage collection
  • Dans les logiciels que j’écris, il y a deux cas : (1) les chemins chauds, où l’on utilise toujours des allocateurs sur mesure et où l’on évite les allocations, (2) tout le reste
    Dans (1), que ce soit du garbage collection ou non ne change rien, je m’en extrairai. Dans (2), le garbage collection est vraiment pratique et correct

    • D’accord. Là où nous utilisions Java et C++ ensemble, nous avons essayé de traiter cette dichotomie par l’interopérabilité, mais au final cela a créé plus de problèmes que cela n’en a résolus
      Le travail accompli par Java avec les garbage collectors modernes est impressionnant, mais même eux reconnaissent indirectement, avec Valhalla, qu’il y a une place pour du code sans allocation ou à faible allocation
  • L’observation selon laquelle les systèmes d’exploitation modernes destinés aux utilisateurs — c’est-à-dire les OS qui ne sont pas des RTOS spécialisés — intègrent un garbage collection passe un peu à côté du sujet ici
    Nous ne l’appelons simplement pas ainsi, nous parlons de gestion de la mémoire. Comment appelle-t-on un langage doté d’un garbage collection intégré ? Un langage à gestion de mémoire
    On voit souvent cela dans les anciens programmes C qui s’exécutent « de haut en bas ». Ils allouent, nettoient les ressources système, mais ne se soucient pas de free. Quand le programme se termine, le système d’exploitation récupère toute cette mémoire, alors pourquoi s’embêter
    Il y aurait là l’occasion de créer un garbage collector au niveau du système d’exploitation, moins isolé du programme, ou un système d’exploitation qui traiterait les ressources comme le fait le garbage collector du runtime d’un langage. Mais, en général, dans les langages à garbage collection, le garbage collector est étroitement imbriqué dans presque toutes les lignes du runtime ; il n’est donc pas pratique de faire en sorte qu’une distribution destinée à un seul système d’exploitation délègue ce contrôle à l’OS
    C’est tout de même dommage. Car il y a beaucoup de marge pour améliorer les problèmes chroniques causés par la séparation artificielle entre gestion de la mémoire au niveau du programme et gestion de la mémoire au niveau du système d’exploitation

    • À la fin d’un programme, comme le système d’exploitation s’en charge, il n’est pas nécessaire de libérer non seulement la mémoire, mais aussi d’autres ressources comme les fichiers, les sockets ou les threads. Sauf si l’on est sur AmigaOS
      La seule raison de libérer la mémoire est, dans une application longue durée, de la réutiliser pour d’autres allocations sans demander davantage de mémoire au système d’exploitation. Pour un outil en ligne de commande exécuté une seule fois puis terminé, ce n’est généralement pas nécessaire
    • On sait que le système d’exploitation ne peut libérer la mémoire qu’à la fin du processus. Il en va de même pour les descripteurs de fichiers et les autres ressources
      Si un processus est conçu pour se terminer une fois son travail accompli, on peut utiliser le système d’exploitation comme un garbage collector
      Mais il n’a jamais existé de moyen pour le système d’exploitation de savoir quelle mémoire n’est plus utilisée à l’intérieur d’un programme en cours d’exécution. À l’exception peut-être de quelques systèmes d’exploitation de recherche ésotériques. Je dirais donc que ce n’est pas tant une occasion manquée que quelque chose dont l’objet même, censément manqué, n’existe pas sous une forme significative
      En revanche, le style de programmation consistant à écrire des programmes très simples et à courte durée de vie est parfaitement légitime. Les outils CLI et les langages de script qui les orchestrent fonctionnent ainsi ; les anciens serveurs web fonctionnaient aussi ainsi avec CGI, etc., et cela reste aujourd’hui une approche tout à fait raisonnable
    • Le Epsilon GC de Java, c’est précisément cela
  • (1) Le passage de RCU à un garbage collection général par traçage donne l’impression d’un appât suivi d’un tour de passe-passe
    (2) La gestion manuelle de la mémoire ne se résume pas aux appels malloc/free, elle concerne aussi l’agencement. Par exemple : séparation des tableaux de structures, inlining, offsets implicites, packing, etc.

    • À propos du point (2), Virgil offre plusieurs mécanismes permettant de contrôler l’agencement mémoire à différents niveaux
      Il semble être question de « tableaux de structures » ; on peut le faire avec des tableaux de tuples, qui sont naturellement aplatis et normalisés selon la cible. Autrement dit, sur une cible native, cela devient un tableau de structures
      On peut aussi définir un agencement exact au niveau de l’octet[1], principalement utilisé pour l’interopérabilité avec d’autres logiciels ou l’analyse de formats binaires. Les types de données algébriques peuvent être déboxés, et il sera bientôt possible de contrôler jusqu’à leur encodage exact
      Virgil utilise le garbage collection
      [1] https://github.com/titzer/virgil/blob/master/doc/tutorial/La...
    • Je ne suis pas d’accord pour dire que le point 2 relève de la gestion manuelle de la mémoire
      Il est clair que les langages managés modernes manquent de contrôle sur l’agencement mémoire, mais les langages bas niveau sont eux aussi loin d’être parfaits, et il existe clairement des moyens d’influencer cet agencement
  • Un point qui manque dans cet article est que async/await s’accorde très bien avec le garbage collection
    Je n’aime pas async/await pour des raisons de style personnel assez particulières, mais je ne vais pas m’étendre
    Je l’ai beaucoup utilisé en TypeScript/JavaScript, ainsi qu’en Dart, où il se comporte comme prévu
    Je l’ai aussi utilisé en Rust, et à mon avis c’est une catastrophe. Forcer le type de gestion mémoire nécessaire pour utiliser async/await dans un runtime multithreadé, c’est l’enfer
    https://doc.rust-lang.org/std/pin/index.html

    • Dans le code asynchrone Rust courant, le pinning n’est pas nécessaire. C’est destiné aux auteurs de bibliothèques
  • Il y a un point que beaucoup de textes en faveur du garbage collection omettent, et cet article semble aussi le faire : la mémoire n’est qu’un type de ressource
    En particulier en programmation système, un code correct doit aussi gérer des ressources externes comme les descripteurs de fichiers, les sockets, etc. Le garbage collection ne résout que la partie mémoire applicative, il n’aide donc absolument pas à traiter ces ressources externes. Il peut même rendre les choses bien plus complexes ; il suffit de voir ce qu’il faut faire pour implémenter correctement IDisposable en .NET dès que ce n’est pas trivial
    Des approches comme RAII ou le comptage de références rendent, d’après mon expérience, bien plus facile le traitement unifié de la mémoire et des ressources externes, ce qui facilite aussi l’écriture de code correct et le raisonnement à son sujet
    Cela ne veut pas dire que je sois ouvertement opposé au garbage collection. Comme tout le reste, c’est un outil avec des avantages et des inconvénients. L’approche RCU de « GC manuel » mentionnée dans l’article est intéressante pour certaines tâches

    • Il existe une grande différence entre la mémoire et les autres ressources. La mémoire, comme la puissance de traitement, est un élément fondamental de tout calcul
      Ce n’est pas pour rien que la plupart des modèles théoriques de calcul supposent une mémoire infinie. Dans certains logiciels, comme les noyaux de systèmes d’exploitation ou les applications temps réel dur, on alloue aussi manuellement la puissance de traitement, mais rares sont les langages qui exigent d’allouer manuellement la puissance de traitement
      Pour des raisons similaires, la gestion automatique de la mémoire est très utile pour abstraire le calcul. Elle empêche les détails mémoire d’une sous-routine de fuiter vers l’appelant ; côté utilisation CPU, ce genre de détail n’apparaît que rarement
      Tout calcul non trivial implique une certaine quantité non constante de traitement et de mémoire, tandis que les E/S se produisent généralement aux frontières du système. La gestion des E/S est bien sûr très importante, mais, dans la notion de calcul et la centralité des abstractions de calcul, elle n’est pas aussi fondamentale que le traitement et la mémoire
    • Exact, et l’argument de la sécurité mémoire s’applique aussi aux autres ressources
      Par exemple, Rust a fini par acquérir une sécurité des E/S, si bien que des handles de fichiers comme OwnedFd sous Unix, ou des handles comme OwnedHandle sous Windows, ne sont plus des entiers comme le nombre 4, mais des objets possédés
      En surface, cela peut ressembler à une histoire d’éviter des erreurs stupides comme faire de l’arithmétique sur des handles ou utiliser à tort des valeurs réservées comme sentinelles, mais grâce au modèle de propriété, même lorsqu’on fait des opérations délicates avec des handles, la propriété est explicite et reste transparente pour les mainteneurs ultérieurs
    • En étant passé surtout de C++ à C#, j’apprécie la gestion mémoire par garbage collection, mais je n’aime pas devoir suivre les descripteurs de fichiers, sockets, etc.
      J’ai vraiment pris la mesure de la valeur de RAII
    • Ce qu’on oublie souvent lorsqu’on critique les langages à garbage collection, c’est que la plupart d’entre eux disposent de fonctionnalités de gestion déterministe des ressources, mais que beaucoup de gens ne les apprennent pas
      Certains langages ont RAII, d’autres fournissent des mots-clés, d’autres encore proposent une gestion de type arène ou des lambdas avec gestion implicite. Certains bénéficient d’un peu d’aide du système de types, d’autres mélangent un peu tout cela
      En outre, tout comme les développeurs système doivent s’appuyer sur des analyseurs statiques, les analyseurs statiques de ces langages peuvent aussi vérifier ce qui a été oublié lorsque le système de types ne suffit pas
    • RAII est évidemment excellent, mais un langage doté du garbage collection et d’une gestion correcte des exceptions peut manipuler les ressources en toute sécurité
      Par exemple, l’instruction try-with-resources de Java garantit que les ressources sont libérées en toute sécurité même en cas d’exception : https://docs.oracle.com/javase/tutorial/essential/exceptions...
      Avec ces briques de base, on peut déjà construire des systèmes assez solides et sûrs du point de vue des ressources