Le garbage collection pour les programmeurs système (2023)
(bitbashing.io)- 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
deleteimmé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-epochpour 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
deleteimmé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()etrcu_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éreroldà 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_ptrou à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_ptrouArcne 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
ncycles 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
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
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.
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 prochainmalloc, mais seulement périodiquementSi l’on alloue 800 octets puis qu’on les
freeimmé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 6276762767, 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
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
freene 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 scepticismeCe 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
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
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éableSurtout 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
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 ?
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
On continue aussi de subir des latences de queue difficiles à prévoir et l’effet de nombreux cas particuliers
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
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
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
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/Arcest 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 collectionDans 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
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êterIl 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 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
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
(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.
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...
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
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
IDisposableen .NET dès que ce n’est pas trivialDes 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
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
Par exemple, Rust a fini par acquérir une sécurité des E/S, si bien que des handles de fichiers comme
OwnedFdsous Unix, ou des handles commeOwnedHandlesous Windows, ne sont plus des entiers comme le nombre 4, mais des objets possédésEn 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
J’ai vraiment pris la mesure de la valeur de RAII
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
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