J'aime vraiment la gestion de la mémoire basée sur la portée (SBMM), ou RAII , car elle est plus communément (confuse?) Mentionnée par la communauté C++. Pour autant que je sache, à l'exception de C++ (et C), il n'y a pas d'autre langage courant utilisé aujourd'hui qui fait de SBMM/RAII leur principal mécanisme de gestion de la mémoire, et à la place ils préfèrent utiliser le garbage collection (GC).
Je trouve cela assez déroutant, car
std::shared_ptr
en C++).Pourquoi le SBMM n'est-il pas plus largement utilisé? Quels sont ses inconvénients?
Commençons par postuler que la mémoire est de loin (des dizaines, des centaines voire des milliers de fois) plus courante que toutes les autres ressources réunies. Chaque variable, objet, membre d'objet a besoin de mémoire qui lui est allouée et libérée plus tard. Pour chaque fichier que vous ouvrez, vous créez des dizaines à des millions d'objets pour stocker les données extraites du fichier. Chaque TCP flux va de pair avec un nombre illimité de chaînes d'octets temporaires créées pour être écrites dans le flux. Sommes-nous sur la même page ici? Génial.
Pour que RAII fonctionne (même si vous avez des pointeurs intelligents prêts à l'emploi pour chaque cas d'utilisation sous le soleil), vous devez obtenir propriété juste. Vous devez analyser qui devrait posséder tel ou tel objet, qui ne devrait pas et quand la propriété devrait être transférée de A à B. Bien sûr, vous pourriez utiliser la propriété partagée pour tout, mais alors vous seriez émuler un GC via des pointeurs intelligents. À ce stade, il devient beaucoup plus facile et plus rapide de construire le GC dans la langue.
La récupération de place vous libère de ce souci pour la ressource de loin la plus utilisée, la mémoire. Bien sûr, vous devez toujours prendre la même décision pour d'autres ressources, mais celles-ci sont beaucoup moins courantes (voir ci-dessus), et la propriété compliquée (par exemple partagée) est également moins courante. La charge mentale est considérablement réduite.
Maintenant, vous nommez certains inconvénients à rendre tous les valeurs récupérées. Cependant, l'intégration des deux types de valeur GC et avec mémoire RAII dans une langue est extrêmement difficile, alors peut-être vaut-il mieux migrer ces compromis via d'autres moyens?
La perte de déterminisme ne s'avère pas si mauvaise en pratique, car elle n'affecte que le déterminisme durée de vie de l'objet. Comme décrit dans le paragraphe suivant, la plupart des ressources (à part la mémoire, qui est abondante et peut être recyclée plutôt paresseusement) sont pas liées à la durée de vie des objets dans ces langages. Il existe quelques autres cas d'utilisation, mais ils sont rares dans mon expérience.
Votre deuxième point, la gestion manuelle des ressources, est aujourd'hui abordé via une instruction qui effectue un nettoyage basé sur la portée, mais ne couple pas ce nettoyage à la durée de vie de l'objet (donc sans interaction avec le GC et la sécurité de la mémoire). Il s'agit de using
en C #, with
en Python, try
- avec-ressources dans les récentes versions Java Java.
RAII découle également de la gestion automatique de la mémoire de comptage des références, par ex. tel qu'utilisé par Perl. Bien que le comptage des références soit facile à implémenter, déterministe et assez performant, il ne peut pas traiter les références circulaires (elles provoquent une fuite), c'est pourquoi il n'est pas couramment utilisé.
Langues récupérées ne peut pas utiliser directement RAII, mais offrent souvent une syntaxe avec un effet équivalent. En Java, nous avons l'instruction try-with-ressource
try (BufferedReader br = new BufferedReader(new FileReader(path))) { ... }
qui appelle automatiquement .close()
sur la ressource à la sortie du bloc. C # possède l'interface IDisposable
, qui permet d'appeler .Dispose()
en quittant une instruction using (...) { ... }
. Python a l'instruction with
:
with open(filename) as f:
...
qui fonctionne de façon similaire. Dans un tour intéressant à ce sujet, la méthode d'ouverture de fichier de Ruby reçoit un rappel. Une fois le rappel exécuté, le fichier est fermé.
File.open(name, mode) do |f|
...
end
Je pense que Node.js utilise la même stratégie.
À mon avis, l'avantage le plus convaincant de la collecte des ordures est qu'il permet composabilité. L'exactitude de la gestion de la mémoire est une propriété locale dans un environnement de récupération de place. Vous pouvez regarder chaque partie isolément et déterminer si elle peut entraîner une fuite de mémoire. Combinez n'importe quel nombre de pièces correctes en mémoire et elles restent correctes.
Lorsque vous comptez sur le comptage des références, vous perdez cette propriété. Si votre application peut fuir la mémoire devient une propriété globale de l'application entière avec comptage de références. Chaque nouvelle interaction entre les pièces a la possibilité d'utiliser le mauvais propriétaire et de rompre la gestion de la mémoire.
Il a un effet très visible sur la conception des programmes dans les différentes langues. Les programmes dans les langages GC ont tendance à être un peu plus de soupes d'objets avec beaucoup d'interactions, tandis que dans les langages sans GC, on a tendance à préférer les parties structurées avec des interactions strictement contrôlées et limitées entre eux.
Les fermetures sont une caractéristique essentielle de presque toutes les langues modernes. Ils sont très faciles à implémenter avec GC et très difficiles (mais pas impossibles) à obtenir correctement avec RAII, car l'une de leurs principales caractéristiques est qu'ils vous permettent d'abstraire pendant la durée de vie de vos variables!
C++ ne les a obtenus que 40 ans après tout le monde, et il a fallu beaucoup de travail acharné par beaucoup de gens intelligents pour les corriger. En revanche, de nombreux langages de script conçus et mis en œuvre par des personnes n'ayant aucune connaissance de la conception et de la mise en œuvre de langages de programmation en disposent.
- SBMM rend les programmes plus déterministes (vous pouvez savoir exactement quand un objet est détruit);
Pour la plupart des programmeurs, le système d'exploitation n'est pas déterministe, leur allocateur de mémoire n'est pas déterministe et la plupart des programmes qu'ils écrivent sont simultanés et, par conséquent, intrinsèquement non déterministes. L'ajout de la contrainte qu'un destructeur est appelé exactement à la fin de la portée plutôt que légèrement avant ou légèrement après n'est pas un avantage pratique significatif pour la grande majorité des programmeurs.
- dans les langues qui utilisent GC, vous devez souvent faire une gestion manuelle des ressources (voir la fermeture de fichiers en Java, par exemple), ce qui va en partie à l'encontre de l'objectif de GC et est également sujet aux erreurs;
Voir using
en C # et use
en F #.
- la mémoire de tas peut également (très élégamment, imo) être liée à la portée (voir std :: shared_ptr en C++).
En d'autres termes, vous pouvez prendre le tas qui est une solution à usage général et le modifier pour ne fonctionner que dans un cas spécifique qui est sérieusement limitant. C'est vrai, bien sûr, mais inutile.
Pourquoi le SBMM n'est-il pas plus largement utilisé? Quels sont ses inconvénients?
SBMM limite ce que vous pouvez faire:
SBMM crée le problème de funarg ascendant avec des fermetures lexicales de première classe, c'est pourquoi les fermetures sont populaires et faciles à utiliser dans des langages comme C # mais rares et délicates en C++. Notez qu'il y a une tendance générale vers l'utilisation de constructions fonctionnelles dans la programmation.
SBMM nécessite des destructeurs et ils empêchent les appels de queue en ajoutant plus de travail à faire avant qu'une fonction puisse revenir. Les appels de queue sont utiles pour les machines à états extensibles et sont fournis par des choses comme .NET.
Certaines structures de données et algorithmes sont notoirement difficiles à implémenter à l'aide de SBMM. Fondamentalement, partout où les cycles se produisent naturellement. Plus particulièrement les algorithmes graphiques. Vous finissez effectivement par écrire votre propre GC.
La programmation simultanée est plus difficile car le flux de contrôle et, par conséquent, les durées de vie des objets sont intrinsèquement non déterministes ici. Les solutions pratiques dans les systèmes de transmission de messages ont tendance à être la copie en profondeur des messages et l'utilisation de durées de vie excessivement longues.
SBMM maintient les objets en vie jusqu'à la fin de leur portée dans le code source, ce qui est souvent plus long que nécessaire et peut être beaucoup plus long que nécessaire. Cela augmente la quantité de déchets flottants (objets inaccessibles en attente de recyclage). En revanche, le traçage de la récupération de place a tendance à libérer des objets peu de temps après la disparition de la dernière référence à ceux-ci, ce qui peut être beaucoup plus tôt. Voir Mythes de la gestion de la mémoire: rapidité .
SBMM est si limitatif que les programmeurs ont besoin d'une voie d'évacuation pour les situations où les durées de vie ne peuvent pas être imbriquées. En C++, shared_ptr
offre une voie d'échappement mais cela peut être environ 10 fois plus lent que le traçage de la récupération de place . Donc, l'utilisation de SBMM au lieu de GC mettrait la plupart des gens sur le mauvais pied la plupart du temps. Cela ne veut pas dire, cependant, qu'il est inutile. SBMM est toujours utile dans le contexte des systèmes et de la programmation embarquée où les ressources sont limitées.
FWIW, vous aimerez peut-être consulter Forth et Ada et lire le travail de Nicolas Wirth.
En regardant un indice de popularité comme TIOBE (ce qui est discutable, bien sûr, mais je suppose que pour votre type de question, il est correct de l'utiliser), vous voyez d'abord que ~ 50% des 20 premiers sont des "langages de script" ou des "dialectes SQL" ", où la" facilité d'utilisation "et les moyens d'abstraction ont bien plus d'importance que le comportement déterministe. Parmi les langues "compilées" restantes, il y a environ 50% des langues avec SBMM et ~ 50% sans. Donc, lorsque vous retirez les langages de script de votre calcul, je dirais que votre hypothèse est tout simplement erronée, parmi les langages compilés, ceux avec SBMM sont aussi populaires que ceux sans.
Un avantage majeur d'un système GC que personne n'a encore mentionné est que ne référence dans un système GC est garantie de conserver son identité tant qu'elle existe. Si l'on appelle IDisposable.Dispose
(.NET) ou AutoCloseable.Close
(Java) sur un objet tant qu'il existe des copies de la référence, ces copies continueront de se référer au même objet. L'objet ne sera plus utile pour rien, mais les tentatives d'utilisation auront un comportement prévisible contrôlé par l'objet lui-même. En revanche, en C++, si le code appelle delete
sur un objet et essaie plus tard de l'utiliser, l'état entier du système devient totalement indéfini.
Une autre chose importante à noter est que la gestion de la mémoire basée sur la portée fonctionne très bien pour les objets avec une propriété clairement définie. Cela fonctionne beaucoup moins bien, et parfois carrément mal, avec des objets qui n'ont pas de propriété définie. En général, les objets modifiables doivent avoir des propriétaires, alors que les objets immuables n'ont pas besoin de l'être, mais il y a une ride: il est très courant que le code utilise une instance d'un type mutable pour contenir des données immuables, en s'assurant qu'aucune référence ne sera exposée à code qui pourrait muter l'instance. Dans un tel scénario, les instances de la classe mutable peuvent être partagées entre plusieurs objets immuables et n'ont donc pas de propriété claire.