Propriété et emprunt en D

Presque tous les programmes non triviaux allouent et utilisent la mémoire dynamique. Le faire correctement devient de plus en plus important car les programmes deviennent plus complexes et les erreurs sont encore plus chères.

Les problèmes typiques sont:

  1. fuites de mémoire (pas de libération de mémoire épuisée)
  2. double libération (libération de la mémoire plus d'une fois)
  3. utilisation après libération (utilisation d'un pointeur sur une mémoire précédemment libérée)

La tâche consiste à suivre les pointeurs responsables de la libération de la mémoire (c'est-à-dire ceux qui en sont propriétaires) et à distinguer les pointeurs qui pointent simplement vers un morceau de mémoire, contrôlent où ils se trouvent et lesquels d'entre eux sont actifs (dans la portée).

Les solutions typiques sont les suivantes:

  1. Garbage Collection (GC) - GC possède des blocs de mémoire et les recherche périodiquement des pointeurs vers ces blocs. Si aucun pointeur n'est trouvé, la mémoire est libérée. Ce schéma est fiable et est utilisé dans des langages tels que Go et Java. Mais GC a tendance à utiliser beaucoup plus de mémoire que nécessaire, a des pauses et ralentit le code en raison du reconditionnement (portes d'écriture insérées à l'origine).
  2. Comptage de références (RC) - Un objet RC possède de la mémoire et stocke un compteur de pointeurs pour lui-même. Lorsque ce compteur diminue à zéro, la mémoire est libérée. C'est également un mécanisme fiable et est accepté dans des langages comme C ++ et ObjectiveC. RC est efficace en mémoire, ne nécessitant en outre que de l'espace sous le comptoir. Les aspects négatifs de RC sont la surcharge de maintenance du compteur, l'incorporation d'un gestionnaire d'exceptions pour assurer sa réduction et le blocage nécessaire pour les objets partagés entre les flux de programme. Pour améliorer les performances, les programmeurs ont parfois trompé en se référant temporairement à un objet RC contournant le compteur, ce qui crée le risque de le faire incorrectement.
  3. Contrôle manuel - La gestion manuelle de la mémoire est Sysalny malloc et gratuite. Il est rapide et efficace en termes d’utilisation de la mémoire, mais le langage n’aide pas à tout faire correctement, s’appuyant entièrement sur l’expérience et le zèle du programmeur. J'utilise malloc et gratuit depuis 35 ans, et avec l'aide d'une expérience amère et sans fin, je fais rarement des erreurs. Mais ce n'est pas de cette façon que la technologie de programmation peut s'appuyer, et notez que j'ai dit "rarement" et non "jamais".

Les solutions 2 et 3 à un degré ou à un autre reposent sur la confiance du programmeur pour tout faire correctement. Les systèmes basés sur la foi n'évoluent pas bien et les erreurs de gestion de la mémoire s'avèrent très difficiles à revérifier (si mauvaises que certaines normes de codage interdisent l'utilisation de la mémoire dynamique).

Mais il existe également une quatrième voie: propriété et emprunt, OB. Il est efficace en mémoire, aussi rapide que le fonctionnement manuel, et est soumis à une nouvelle vérification automatique. La méthode a récemment été popularisée par le langage de programmation Rust. Elle présente également ses inconvénients, notamment la nécessité de repenser la planification des algorithmes et des structures de données.

Vous pouvez traiter des aspects négatifs et le reste de cet article est une description schématique du fonctionnement du système OB et de la façon dont nous proposons de l'écrire dans le langage D. J'ai d'abord considéré cela comme impossible, mais après y avoir réfléchi, j'ai trouvé un moyen. C'est similaire à ce que nous avons fait avec la programmation fonctionnelle - avec l'immuabilité transitive et les fonctions "pures".

Possession


La décision de qui détient l'objet en mémoire est ridiculement simple - il n'y a qu'un seul pointeur sur l'objet et c'est le propriétaire. Il est également responsable de la libération de la mémoire, après quoi celle-ci devient invalide. En raison du fait que le pointeur vers l'objet en mémoire est le propriétaire, il n'y a pas d'autres pointeurs à l'intérieur de cette structure de données, et donc la structure de données forme un arbre.

La deuxième conséquence est que les pointeurs utilisent la sémantique du déplacement plutôt que de la copie:

T* f(); void g(T*); T* p = f(); T* q = p; //  p   q,    g(p); // , p   

Il est interdit de supprimer un pointeur de l'intérieur d'une structure de données:
 struct S { T* p; } S* f(); S* s = f(); T* q = sp; // ,      sp 

Pourquoi ne pas simplement marquer sp comme invalide? Le problème est que cela nécessitera de définir l'étiquette en runtime, mais devrait être résolu au stade de la compilation, car il est simplement considéré comme une erreur de compilation.

La sortie du propre pointeur hors de portée est également une erreur:

 void h() { T* p = f(); } // ,   p? 

Vous devez déplacer la valeur du pointeur différemment:
 void g(T*); void h() { T* p = f(); g(p); //   g(),    g() } 

Cela résout les problèmes de fuite de mémoire et d'utilisation après la libération (indice: pour plus de clarté, remplacez f () par malloc () et g () par free ().)

Tout cela peut être vérifié au stade de la compilation à l'aide de la technique d' analyse de flux de données (DFA) , tout comme elle est utilisée pour supprimer les sous-expressions courantes . DFA peut dénouer tout enchevêtrement de rats des transitions de programme qui peuvent survenir.

Emprunter


Le régime foncier décrit ci-dessus est fiable, mais trop restrictif.
Considérez:

 struct S { void car(); void bar(); } struct S* f(); S* s = f(); s.car(); // s   car() s.bar(); // , s  

Pour que cela fonctionne, s.car () doit avoir un moyen de récupérer le pointeur à la sortie.

Voilà comment fonctionne l'emprunt. s.car () prend une copie de s pour la durée de s.car (). s n'est pas valide au moment de l'exécution et redevient valide lorsque s.car () se ferme.

En D, les fonctions membres struct obtiennent le pointeur this par référence, afin que nous puissions adapter l'emprunt avec une petite extension: obtenir l'argument par référence le prend.

D prend également en charge la portée des pointeurs, l'emprunt est donc naturel:

 void g(scope T*); T* f(); T* p = f(); g(p); // g()  p g(p); //    p     g() 

(Lorsque les fonctions reçoivent des arguments par référence ou que des pointeurs avec portée sont utilisés, il leur est interdit de s'étendre au-delà des limites d'une fonction ou d'une portée. Cela correspond à la sémantique de l'emprunt.)

Emprunter de cette manière garantit l'unicité d'un pointeur sur un objet en mémoire à un instant donné.

L'emprunt peut être étendu davantage en sachant que le système de propriété est également fiable, même si un objet est en outre indiqué par plusieurs pointeurs constants (mais un seul mutable). Un pointeur constant ne peut pas changer la mémoire ni la libérer. Cela signifie que plusieurs pointeurs constants peuvent être empruntés au propriétaire mutable, mais il n'a pas le droit d'être utilisé tant que ces pointeurs constants sont vivants.

Par exemple:

 T* f(); void g(T*); T* p = f(); // p   { scope const T* q = p; //    scope const T* r = p; //    g(p); // , p   q  r    } g(p); // ok 

Principes


Ce qui précède peut être réduit à la compréhension suivante qu'un objet en mémoire se comporte comme s'il était dans l'un des deux états suivants:

  1. il y a exactement un pointeur mutable
  2. un ou plusieurs pointeurs constants supplémentaires

Un lecteur attentif remarquera quelque chose d'étrange dans ce que j'ai écrit: «comme si». À quoi voulais-je faire allusion? Que se passe-t-il? Oui, il y en a un. Les langages de programmation informatique sont pleins de «comme si» sous le capot, quelque chose comme l'argent dans votre compte bancaire n'est en fait pas là (je m'excuse si cela a été un choc brutal pour quelqu'un), et ce n'est pas différent de cela. Continuez à lire!

Mais d'abord, un peu plus profondément dans le sujet.

Intégration des techniques de propriété / emprunt dans D


Ces techniques ne sont-elles pas incompatibles avec la façon dont les gens écrivent habituellement en D, et presque tous les programmes D existants ne se cassent-ils pas? Et ce n'est pas si facile à réparer, mais tellement que vous devez repenser tous les algorithmes à partir de zéro?

Oui en effet. A moins que D n'ait une arme (presque) secrète: les attributs des fonctions. Il s'avère que la sémantique de propriété / emprunt (OB) peut être implémentée séparément pour chaque fonction après l'analyse sémantique habituelle. Un lecteur attentif pourrait remarquer qu'aucune nouvelle syntaxe n'a été ajoutée, seules des restrictions ont été imposées sur le code existant. D a déjà un historique d'utilisation des attributs de fonction pour changer leur sémantique, par exemple, l'attribut pur pour créer des fonctions «pures». Pour activer la sémantique OB, l'attribut @ live est ajouté.

Cela signifie que l'OB peut être ajouté au code sur D progressivement, selon les besoins et les ressources gratuites. Cela permet d'ajouter des OB, ce qui est essentiel, en soutenant constamment le projet dans un état pleinement fonctionnel, testé et prêt à être publié. Il vous permet également d'automatiser le processus de surveillance du pourcentage du projet qui a déjà été transféré à l'OB. Cette technique est ajoutée à la liste des autres garanties du langage D concernant la fiabilité du travail avec la mémoire (comme le contrôle de la non-distribution des pointeurs sur les variables temporaires de la pile).

Comme si


Certaines choses nécessaires ne peuvent pas être réalisées avec le strict respect des OB, tels que les objets de comptage de références. Après tout, les objets RC sont conçus pour avoir de nombreux pointeurs vers eux. Étant donné que les objets RC sont sûrs lors de l'utilisation de la mémoire (s'ils sont correctement mis en œuvre), ils peuvent être utilisés avec des OB sans affecter négativement la fiabilité. Ils ne peuvent tout simplement pas être créés à l'aide de la technique OB. La solution est qu'il existe d'autres attributs de fonction dans D, comme @ system . @ system sont des fonctionnalités où de nombreux contrôles de fiabilité sont désactivés. Naturellement, l'OB sera également désactivé dans le code avec @ system . C'est là que la mise en œuvre de la technologie RC se cache du contrôle OB.

Mais dans le code avec OB, RC, l'objet a l'air de suivre toutes les règles, donc pas de problème!

Il faudra un certain nombre de types de bibliothèques similaires pour fonctionner correctement avec OB.

Conclusion


Cet article est un aperçu de base de la technologie OB. Je travaille sur une spécification beaucoup plus détaillée. Il est possible que j'ai raté quelque chose et quelque part un trou sous la ligne de flottaison, mais jusqu'à présent, tout semble bien. Il s'agit d'un développement très excitant pour D et j'ai hâte de le mettre en œuvre.

Pour plus de discussions et commentaires de Walter, reportez-vous aux rubriques sur / r / programmation subreddit et sur Hacker News .

Source: https://habr.com/ru/post/fr460671/


All Articles