- Tree Borrows, un nouveau modèle mémoire visant à dépasser les limites d’optimisation du code
unsafe en Rust, est proposé
- Là où l’approche Stacked Borrows n’autorisait pas plusieurs motifs courants dans le code Rust en production, Tree Borrows résout le problème grâce à une structure en arbre, en offrant des règles plus réalistes et plus souples
- Tree Borrows fait passer 54 % de cas de test de code réel en plus que Stacked Borrows
- Il préserve l’essentiel des garanties de sûreté mémoire et des possibilités d’optimisation de Rust (notamment le read-read reordering), tout en intégrant les fonctionnalités avancées du borrow checker moderne de Rust
- Il introduit un modèle de machine à états fondé sur un arbre, marquant une étape importante pour la recherche sur l’optimisation de Rust et la vérification de la sûreté
Le système de possession de Rust et les limites du code unsafe
- Rust offre de fortes garanties, comme la sûreté mémoire et la prévention des data races, grâce à son système de types fondé sur la possession
- Mais Rust comporte aussi une unsafe escape hatch, dans laquelle la vérification de la sûreté ne relève plus du compilateur mais du développeur
- Le compilateur aimerait exploiter des règles d’aliasing de pointeurs pour appliquer des optimisations agressives, mais un code
unsafe incorrect peut rendre ces optimisations impossibles
Stacked Borrows et ses limites
- Jusqu’ici, le modèle Stacked Borrows définissait les « comportements incorrects » du code
unsafe et servait de référence pour les optimisations
- Mais cette approche n’autorise pas plusieurs motifs
unsafe courants dans le code Rust réel, et ne reflète pas non plus les fonctionnalités récentes du borrow checker de Rust
L’arrivée de Tree Borrows
- Tree Borrows est un nouveau modèle qui suit les permissions mémoire au moyen d’une structure en arbre plutôt qu’en pile
- Il permet ainsi d’autoriser en toute sûreté davantage de motifs de code Rust utilisés en pratique, en améliorant nettement la souplesse des règles de borrow et leur applicabilité au monde réel
- Lors d’une évaluation sur 30 000 crates Rust populaires, il fait passer 54 % de cas de test en plus que Stacked Borrows
Caractéristiques et avantages de Tree Borrows
- Il conserve l’essentiel des optimisations majeures de Stacked Borrows (par ex. les
read-read reorderings)
- Il peut aussi prendre en compte les fonctionnalités avancées du borrow checker moderne de Rust (par ex. des motifs de borrow atypiques, des manipulations de pointeurs complexes, etc.)
- Il introduit un modèle de machine à états fondé sur un arbre afin d’équilibrer sûreté et potentiel d’optimisation
Conclusion et portée
- Tree Borrows établit une nouvelle référence pour le traitement du code
unsafe et la recherche sur l’optimisation dans le compilateur Rust
- Il est considéré comme un modèle mémoire réaliste et robuste, couvrant à la fois le code Rust en production et les politiques récentes du borrow checker
- L’article scientifique, les artefacts et le code source associés sont publiés, et devraient avoir un impact important sur la communauté de recherche autour du compilateur Rust et de la vérification
1 commentaires
Commentaires Hacker News
Le récent billet de blog de Ralf Jung apporte davantage de contexte lien
Bonus : je recommande aussi la récente présentation du groupe de recherche qui tente de spécifier clairement la sémantique d’exécution de Rust dans un dialecte de Rust YouTube
On affirme que, du point de vue du compilateur, les fortes garanties du système de types sur l’aliasing de pointeurs permettent des optimisations puissantes, mais je me demande dans quelle mesure c’est réellement efficace
Linus Torvalds soutient depuis longtemps que les règles de strict aliasing du C sont de peu d’utilité et causent surtout des problèmes
Son billet avec un exemple est aussi intéressant
Je me demande si Rust est fondamentalement différent de C sur ce point ; d’après mon expérience personnelle, surtout quand
unsafeentre en jeu, la différence ne me paraît pas énormeJe trouve vraiment que les règles de strict aliasing du C sont mauvaises
Les règles proposées par Rust sont très différentes, plus utiles pour le compilateur, et moins contraignantes pour le programmeur
On peut s’en abstraire dans le langage en utilisant des raw pointers, et il existe aussi des outils pour vérifier le code
Au final, comme tout choix de conception de langage, c’est un compromis
Rust semble avoir trouvé un nouvel équilibre dans cette zone d’optimisation, et le temps dira si ce jugement était le bon
Les règles d’aliasing de Rust sont très différentes de celles du C
En C, le mot-clé
restrictn’a pratiquement de sens que pour les arguments de fonction, et l’aliasing basé sur les types (type-based aliasing) est en pratique peu utilisé ou peu pratique à utiliserEn Rust, on peut exprimer finement les durées de vie et la mutabilité, et manipuler la mémoire de manière sûre de nombreuses façons indépendamment du type lui-même
Le fait qu’il suffise d’éviter des références
&mutqui se chevauchent, tout en pouvant découper en plusieurs&mutnon recouvrantes, est un point importantJ’aimerais voir une analyse plus large de l’impact réel sur les performances
On pourrait le savoir assez vite en supprimant simplement du compilateur toute la partie qui transmet les informations d’aliasing à LLVM, puis en comparant les performances
Il y a aussi l’affirmation selon laquelle l’annotation
noaliasaméliore les performances d’environ 5 % à l’exécution ; voir ce commentaire (même si les données sont anciennes)Il faut prendre avec recul ce que dit Linus sur les compilateurs
Un noyau OS et un compilateur, ce sont deux domaines complètement différents
Aujourd’hui, l’analyse d’aliasing est vraiment au cœur de fortes améliorations de performances
Les gains les plus importants viennent d’heuristiques simples, et les requêtes d’aliasing complexes ont en elles-mêmes une utilité plus limitée
J’aimerais faire une expérience pour voir de combien une analyse d’aliasing théoriquement parfaite améliorerait les performances, mais j’ai l’impression qu’on atteindrait une limite d’environ 20 % même sur du code général
Bien sûr, certaines optimisations très avancées (par ex. les transformations de layout de données) ont aussi la limite qu’on n’essaiera même pas de les faire sans analyse d’aliasing
Le strict aliasing du C et l’aliasing de Rust sont des concepts différents
En C, le cœur du sujet est la TBAA (type-based analysis aliasing), et Rust a délibérément choisi de ne pas l’adopter
Il y a eu d’anciens fils sur Stacked Borrows en 2020 et 2018
Fil 2020
Fil 2018
J’ai lu la spécification de Tree Borrows sur le site de Nevin il y a quelques années, et j’avais été impressionné par la façon élégante dont elle résolvait des problèmes complexes
En pratique, Tree Borrows permet du code raisonnable qui n’est pas possible avec Stacked Borrows
Le code d’exemple de la bibliothèque standard de Rust vaut aussi le détour
Je me demande si Rust, ou un langage de programmation de nouvelle génération, pourrait évoluer pour permettre de choisir entre plusieurs implémentations de borrow checker aux caractéristiques et objectifs différents (vitesse de compilation, vitesse d’exécution, flexibilité algorithmique, etc.)
Rust prend déjà en charge le changement d’implémentation du borrow checker
On est passé d’un modèle fondé sur la portée à un modèle non lexical, et Polonius est aussi disponible comme implémentation expérimentale
Quand une nouvelle implémentation est prête, il n’y a pas vraiment de raison de conserver l’ancienne
On peut aussi écrire de façon plus souple avec
Rc,RefCell, etc., qui nécessitent des vérifications à l’exécutionIl existe déjà diverses approches : types affines (utilisés par Rust), types linéaires, systèmes d’effets, types dépendants, preuves formelles, etc.
Chaque approche a ses propres caractéristiques en matière de coût d’implémentation, de performances, d’expérience développeur, etc.
Au-delà de Rust, il y a aussi une tendance à rechercher la combinaison entre gestion automatique productive des ressources et système de types
Ce qu’il faut réellement, c’est une logique de séparation permettant de spécifier précisément les préconditions d’une fonction et même de prouver les conditions intermédiaires
L’approche de Rust consiste à systématiser les invariants ordinaires que les gens veulent réellement, tout en garantissant de fortes optimisations
Je me demande si le résultat du borrow checker n’a que des faux négatifs et pas de faux positifs
Si c’est le cas, est-ce qu’on pourrait faire tourner plusieurs implémentations en parallèle sur différents threads et utiliser le résultat du gagnant le plus rapide ?
Autoriser simultanément plusieurs implémentations de borrow checker risquerait de fragmenter l’écosystème, donc ce ne serait pas souhaitable
J’ai effectivement testé le code Rust présenté dans l’article, et j’ai confirmé qu’il n’est pas rejeté par le compilateur stable le plus récent
Exemple de code :
Si on exécute le code ci-dessus dans miri, il signale une erreur sur
*x = 10;, maiswrite(x);ne provoque pas d’erreurDu point de vue du système de types, rustc n’a aucune raison de rejeter l’une ou l’autre version, puisque
yest de type*mutL’article donne l’exemple suivant comme problème de code
unsafe:Je me demande si cela est réellement possible
Utiliser plusieurs références mutables vers la même variable via des pointeurs est clairement de l’UB, donc je me demande si je ne passe pas à côté de quelque chose
Le code ci-dessus enfreint les règles, même si le compilateur Rust l’accepte
Quelles règles ?
Le code qui passe le borrow checker est légal
unsafepeut aussi exprimer des choses illégales / de l’UBIl existe un ensemble de règles plus large que le périmètre du borrow checker, mais qui reste légal
Le but de cette recherche est de définir rigoureusement cette frontière
L’article sur Stacked Borrows était plus simple, mais il avait des limites avec le vrai code
unsafe, et Tree Borrows reconnaît un périmètre de sûreté plus largeIl est clair que « plusieurs pointeurs de références mutables ne peuvent pas exister simultanément », mais je n’ai vu nulle part une mention explicite de la règle exacte que cela violerait
Tree Borrows propose précisément ce genre de définition
Quand on dit que « le code peut faire ce genre de chose », cela signifie qu’on peut réellement écrire et exécuter ce code, mais que sans une définition comme Tree Borrows il est difficile d’établir pourquoi c’est incorrect
On dirait que tu reconnais déjà toi-même la nécessité de règles claires comme celles de Tree Borrows
Du code
unsafecomme cela est bel et bien possible en pratique, et c’est justement pour cela que c’est de l’UBExemple : lien playground
Si tu veux le contexte, le début du paragraphe suivant dans l’article exprime bien l’intention
Oui, c’est exactement le point
Cela reflète à quel point il est difficile de respecter la règle interdisant plusieurs références mutables, et combien
unsafepeut autoriser davantage de choses que ce que garantit le système de lifetimes de RustL’un des auteurs, Neven Villani, est le fils de Cédric Villani, médaille Fields 2010
On pense à l’expression selon laquelle les chiens ne font pas des chats
Et j’avais aussi envie de faire le trait d’esprit selon lequel « les qualités aussi ont été empruntées à l’arbre »
J’ai même eu un bureau proche de celui du père (lauréat de la médaille Fields) à une époque
C’était avant son entrée en politique
Ce modèle est vraiment excellent
Je vais aussi l’implémenter dans le langage que je développe
Impossible que ce soit un déjà-vu
J’ai l’impression de revoir ce post tous les 2 ou 3 mois