J'ai récemment regardé une excellente conférence de Herb Sutter sur "Leak Free C++ ..." à la CppCon 2016, où il a parlé de l'utilisation de pointeurs intelligents pour implémenter RAII (Concepts d'acquisition de ressources, c'est l'initialisation) et comment ils résolvent la plupart des problèmes de fuite de mémoire.
Maintenant je me demandais. Si je suis strictement les règles RAII, ce qui semble être une bonne chose, pourquoi cela serait-il différent d'avoir un ramasse-miettes en C++? Je sais qu'avec RAII, le programmeur contrôle parfaitement le moment où les ressources sont libérées à nouveau, mais est-ce que cela est en tout cas avantageux pour un simple ramasse-miettes? Serait-ce vraiment moins efficace? J'ai même entendu dire qu'avoir un ramasse-miettes peut être plus efficace, car il peut libérer de gros morceaux de mémoire à la fois au lieu de libérer de petits morceaux de mémoire dans tout le code.
Si je suis strictement les règles RAII, ce qui semble être une bonne chose, pourquoi cela serait-il différent d'avoir un ramasse-miettes en C++?
Bien que les deux traitent des allocations, ils le font de manière complètement différente. Si vous vous adressez à un GC tel que celui de Java, cela ajoute une charge supplémentaire, supprime une partie du déterminisme du processus de libération des ressources et gère les références circulaires.
Vous pouvez implémenter le GC, mais dans des cas particuliers, avec des caractéristiques de performances très différentes. J'ai mis en place une seule fois les connexions de socket, dans un serveur hautes performances/à haut débit (le simple appel de l'API de fermeture de socket prenait trop de temps et ralentissait les performances de débit). Cela impliquait pas de mémoire, mais des connexions réseau, et pas de traitement de dépendance cyclique.
Je sais qu'avec RAII, le programmeur contrôle parfaitement le moment où les ressources sont libérées à nouveau, mais est-ce que cela est en tout cas avantageux pour un simple ramasse-miettes?
Ce déterminisme est une caractéristique que GC ne permet tout simplement pas. Parfois, vous voulez pouvoir savoir qu'après un certain temps, une opération de nettoyage a été effectuée (suppression d'un fichier temporaire, fermeture d'une connexion réseau, etc.).
Dans de tels cas, GC ne le coupe pas, ce qui est la raison en C # (par exemple) vous avez l'interface IDisposable
.
J'ai même entendu dire qu'avoir un ramasse-miettes peut être plus efficace, car il peut libérer de gros morceaux de mémoire à la fois au lieu de libérer de petits morceaux de mémoire dans le code.
Peut-être ... dépend de la mise en œuvre.
Le ramassage des ordures résout certaines catégories de problèmes de ressources que RAII ne peut pas résoudre. En gros, cela se résume à des dépendances circulaires dans lesquelles vous n'identifiez pas le cycle à l'avance.
Cela lui donne deux avantages. Premièrement, il y aura certains types de problèmes que RAII ne peut pas résoudre. Celles-ci sont, selon mon expérience, rares.
Le plus important est que cela laisse le programmeur être paresseux et indifférent à propos des durées de vie des ressources de mémoire et de certaines autres ressources pour lesquelles le nettoyage différé ne vous dérange pas. Lorsque vous n'avez pas à vous soucier de certains types de problèmes, vous pouvez vous soucier de {plus} _ autres problèmes. Cela vous permet de vous concentrer sur les parties de votre problème sur lesquelles vous souhaitez vous concentrer.
L'inconvénient est que sans RAII, la gestion des ressources dont vous souhaitez limiter la durée de vie est difficile. Les langages GC vous réduisent au minimum à des durées de vie extrêmement simples ou à la gestion manuelle des ressources, comme en C, en indiquant manuellement que vous avez terminé avec une ressource. Leur système de durée de vie des objets est fortement lié au GC et ne fonctionne pas bien pour une gestion de la durée de vie serrée de systèmes complexes de grande taille (mais sans cycle).
Pour être juste, la gestion des ressources en C++ nécessite beaucoup de travail pour fonctionner correctement dans des systèmes aussi vastes et complexes (sans cycle). C # et les langages similaires rendent la touche plus difficile, en échange ils facilitent la tâche.
La plupart des mises en œuvre du GC obligent également les classes à part entière non localisées; la création de tampons contigus d'objets généraux ou la composition d'objets généraux en un objet plus grand n'est pas une tâche facile pour la plupart des implémentations du GC. D'autre part, C # vous permet de créer un type de valeur struct
s avec des capacités quelque peu limitées. À l’ère actuelle de l’architecture de la CPU, la convivialité du cache est essentielle et l’absence de force imposée par la localisation est un lourd fardeau. Comme ces langages ont pour la plupart une exécution en bytecode, l’environnement JIT pourrait théoriquement transférer des données couramment utilisées ensemble, mais le plus souvent vous obtiendrez une perte de performance uniforme en raison de fréquents manquements de cache par rapport au C++.
Le dernier problème avec GC est que la désallocation est indéterminée et peut parfois causer des problèmes de performances. Les GC modernes rendent ce problème moins problématique que par le passé.
RAII et GC résolvent des problèmes dans des directions complètement différentes. Ils sont complètement différents, malgré ce que certains diraient.
Les deux traitent du problème que la gestion des ressources est difficile. Garbage Collection résout le problème en faisant en sorte que le développeur n'ait pas à accorder autant d'attention à la gestion de ces ressources. RAII le résout en facilitant l’attention des développeurs pour la gestion de leurs ressources. Toute personne qui dit faire la même chose a quelque chose à vous vendre.
Si vous examinez les dernières tendances linguistiques, vous constatez que les deux approches sont utilisées dans la même langue car, franchement, vous avez réellement besoin des deux côtés du puzzle. Vous voyez beaucoup de langages qui utilisent le tri des ordures pour ne pas avoir à vous soucier de la plupart des objets. Ces langages offrent également des solutions RAII (telles que l'opérateur with
de python) pour les moments auxquels vous voulez vraiment faire attention. pour eux.
shared_ptr
(Si je puis me permettre, le refcounting et le GC appartiennent à la même classe de solutions, car elles sont toutes deux conçues pour vous aider à ne pas prêter attention à la durée de vie)with
et GC via un système de décompte en plus d’un ramasse-miettes.IDisposable
et using
et GC via un ramasse-miettes générationnelLes modèles se retrouvent dans toutes les langues.
L’un des problèmes des éboueurs est qu’il est difficile de prévoir les performances des programmes.
Avec RAII, vous savez que, dans le temps exact, les ressources vont sortir du cadre, vous allez effacer de la mémoire et cela prendra du temps. Mais si vous n'êtes pas un maître des paramètres du ramasse-miettes, vous ne pouvez pas prédire quand le nettoyage aura lieu.
Par exemple: le nettoyage de petits objets peut être effectué plus efficacement avec GC, car il peut libérer de gros morceaux, mais l'opération ne sera pas rapide et il est difficile de prédire quand le processus entrera et à cause du "nettoyage de gros morceaux", prendre un peu de temps processeur et peut affecter les performances de votre programme.
Grosso modo. L'idiome RAII peut être meilleur pour latency et jitter. Un ramasse-miettes peut être préférable pour le système débit.
La collecte des ordures et RAII prennent en charge une construction commune pour laquelle l’autre n’est pas vraiment appropriée.
Dans un système collecté avec des déchets, le code peut traiter efficacement les références à des objets immuables (telles que des chaînes) comme des mandataires pour les données qui y sont contenues; Passer de telles références est presque aussi économique que de passer par des pointeurs "idiots" et est plus rapide que de faire une copie séparée des données pour chaque propriétaire ou d'essayer de suivre la propriété d'une copie partagée des données. En outre, les systèmes collectés avec des ordures facilitent la création de types d'objets immuables en écrivant une classe qui crée un objet mutable, en le renseignant à votre guise et en fournissant des méthodes d'accesseur, tout en évitant les fuites de références vers tout élément susceptible de le transformer une fois que le constructeur l'a modifié. se termine. Dans les cas où les références aux objets immuables doivent être largement copiées alors que les objets eux-mêmes ne le sont pas, GC bat RAII haut la main.
D'autre part, RAII est excellent pour gérer les situations dans lesquelles un objet doit acquérir des services exclusifs auprès d'entités extérieures. Bien que de nombreux systèmes du GC permettent aux objets de définir des méthodes de "finalisation" et de demander une notification s’ils sont abandonnés, et que de telles méthodes parviennent parfois à libérer des services extérieurs qui ne sont plus nécessaires, elles sont rarement suffisamment fiables pour fournir un moyen satisfaisant de fonctionner. assurer la libération en temps voulu des services extérieurs. Pour la gestion des ressources extérieures non fongibles, RAII bat le GC haut la main.
La principale différence entre les cas où GC gagne et ceux où RAII gagne est que GC est capable de gérer une mémoire fongible pouvant être libérée selon les besoins, mais médiocre pour gérer des ressources non fongibles. RAII est doué pour manipuler des objets avec une propriété claire, mais mal pour manipuler des détenteurs de données immuables sans propriétaire, qui n’ont pas d’identité réelle en dehors des données qu’ils contiennent.
Étant donné que ni GC ni RAII ne gèrent correctement tous les scénarios, il serait utile que les langues fournissent un bon support pour les deux. Malheureusement, les langues qui se concentrent sur l’une ont tendance à traiter l’autre comme une réflexion après coup.
La partie principale de la question de savoir si l’un ou l’autre est "bénéfique" ou plus "efficace" ne peut être résolue sans donner beaucoup de contexte et de débattre sur les définitions de ces termes.
Au-delà de cela, vous pouvez ressentir la tension de l'ancien "Java ou C++ est-il le meilleur langage?" flamme crépitant dans les commentaires. Je me demande à quoi une réponse "acceptable" à cette question pourrait ressembler, et je suis curieux de la voir éventuellement.
Mais un point concernant une différence conceptuelle potentiellement importante n'a pas encore été signalé: avec RAII, vous êtes lié au fil qui appelle le destructeur. Si votre application est à thread unique (et même si c'est Herb Sutter qui a déclaré que Le déjeuner gratuit est terminé : Aujourd'hui, la plupart des logiciels sont encore en simple est mono-threaded), occupé à gérer le nettoyage des objets qui ne sont plus pertinents pour le programme réel ...
Contrairement à cela, le ramasse-miettes s'exécute généralement dans son propre thread, voire dans plusieurs threads, et est donc (dans une certaine mesure) découplé de l'exécution des autres parties.
(Remarque: certaines réponses ont déjà tenté de mettre en évidence des modèles d'application présentant des caractéristiques différentes, telles que l'efficacité, les performances, le temps de latence et le débit, mais ce point spécifique n'a pas encore été mentionné.)
"Efficace" est un terme très large, au sens des efforts de développement. RAII est généralement moins efficace que GC, mais en termes de performance, GC est généralement moins efficace que RAII. Cependant, il est possible de fournir des exemples de contrôle pour les deux cas. Traiter avec le GC générique lorsque vous avez des modèles de désaffectation de ressources très clairs dans les langues gérées peut s'avérer plutôt gênant, tout comme le code utilisant RAII peut être étonnamment inefficace lorsque shared_ptr
est utilisé pour tout, sans raison.
RAII traite uniformément tout ce qui peut être décrit comme une ressource. Les allocations dynamiques sont une de ces ressources, mais elles ne sont en aucun cas les seules, et on peut dire que pas la plus importante. Les fichiers, les sockets, les connexions à la base de données, les commentaires d'interface graphique, etc. sont autant de choses qui peuvent être gérées de manière déterministe avec RAII.
Les GC ne traitent que des allocations dynamiques, ce qui évite aux programmeurs de se préoccuper du volume total d'objets alloués au cours de la durée de vie du programme (ils doivent uniquement se préoccuper de l'ajustement du volume d'allocation simultanée maximum).
RAII et le ramassage des ordures visent à résoudre différents problèmes.
Lorsque vous utilisez RAII, vous laissez un objet sur la pile dont le seul but est de nettoyer ce que vous voulez gérer (sockets, mémoire, fichiers, etc.) en quittant le domaine d'application de la méthode. Ceci est pour exception-safety , et pas seulement pour la récupération de place, c'est pourquoi vous obtenez des réponses sur la fermeture de sockets et la libération de mutex, etc. (Très bien, personne ne m'a parlé de mutexes à part moi.) Si une exception est levée, le dépilage de pile nettoie naturellement les ressources utilisées par une méthode.
Le ramassage des ordures est la gestion programmatique de la mémoire, bien que vous puissiez "ramasser" les autres ressources rares si vous le souhaitez. Les libérer explicitement a plus de sens 99% du temps. La seule raison d'utiliser RAII pour quelque chose comme un fichier ou un socket est que vous vous attendez à ce que l'utilisation de la ressource soit complète au retour de la méthode.
La récupération de place traite également des objets alloués au tas , par exemple lorsqu'une fabrique construit une instance d'un objet et la renvoie. Avoir des objets persistants dans des situations où le contrôle doit laisser une portée est ce qui rend le ramassage des ordures attrayant. Mais vous pouvez utiliser RAII dans l'usine. Ainsi, si une exception est générée avant votre retour, vous ne perdez pas de ressources.
J'ai même entendu dire qu'avoir un ramasse-miettes peut être plus efficace, car il peut libérer de gros morceaux de mémoire à la fois au lieu de libérer de petits morceaux de mémoire dans tout le code.
C’est parfaitement faisable - et, en fait, c’est fait - avec RAII (ou avec plain malloc/free). Vous voyez, vous n'utilisez pas nécessairement toujours l'allocateur par défaut, qui ne désalloue que par morceaux. Dans certains contextes, vous utilisez des allocateurs personnalisés avec différents types de fonctionnalités. Certains allocateurs ont la capacité intrinsèque de tout libérer dans une région d’allocateur, sans avoir à itérer des éléments alloués individuels.
Bien sûr, vous abordez ensuite la question de savoir quand tout désallouer - que l’utilisation de ces allocateurs (ou de la dalle de mémoire à laquelle ils sont associés doit être RAIIed ou non, et comment.