Résoudre un problème de performance vieux de 3 ans dans Gleam avec une arena Rust
(giacomocavalieri.me)- Dans la structure récursive
Documentdu formateur Gleam, les allocations individuelles sur le tas répétées ont été remplacées par des références basées sur une arena, résolvant ainsi un problème d’amélioration des performances resté ouvert pendant 3 ans Box<Self>a été remplacé par&Selfstocké dans une arena, et les vérifications de durée de vie de Rust garantissent que les données internes ne sont pas référencées après la suppression de l’arena- Des centaines de
Documentrécurrents, comme les mots-clés du langage et les virgules, sont désormais alloués une seule fois puis réutilisés, ce qui réduit le temps d’exécution du pretty-printer de 13 ms à 9,8 ms, soit 24 % plus rapide - Le temps d’exécution total de
gleam format, incluant la lecture et le parsing des sources, a également été réduit de 13 %, tandis que l’utilisation maximale de mémoire est passée de 8,4 Mo à 7,6 Mo, soit environ 10 % de moins - Il a fallu modifier largement les fonctions et leurs sites d’appel pour passer l’arena, avec un travail manuel de l’ordre de
+2963/-1032, mais cette approche consistant à réduire les allocations individuelles répétées a permis d’améliorer à la fois la vitesse et l’usage mémoire
Le coût des allocations dans une structure Document récursive
- Le pretty-printer de Gleam utilise une structure de données
Documentrécursive qui décrit comment afficher le code et où le couper lorsque la limite de ligne est dépassée - Ses principaux variants comprennent
String, qui contient la chaîne à afficher,Break, qui définit les chaînes avant et après un saut de ligne,Group, qui regroupe plusieurs documents, etNest, qui augmente l’indentation lors d’un saut de ligne - Même une liste
[1, 2]est représentée par une combinaison deString,Break,NestetGroup- Si elle tient sur la ligne courante, elle est affichée sur une seule ligne, comme
[1, 2] - Si elle dépasse la limite de ligne, elle est répartie sur plusieurs lignes selon les
Breakdéfinis, avec indentation et affichage de la virgule finale
- Si elle tient sur la ligne courante, elle est affichée sur une seule ligne, comme
- Les variants qui contiennent un autre
Document, commeNest, utilisaient auparavantBox<Self>, ce qui imposait une allocation individuelle sur le tas pour chaque document imbriqué, une opération pouvant représenter une part importante du temps d’exécution - L’algorithme de base du pretty-printer de Gleam est décrit dans l’article Strictly Pretty
Remplacer Box par des références vers une arena
- L’ancien
Nest(Box<Self>)a été remplacé parNest(&'doc Self), afin que la structure référence un autreDocumentstocké dans l’arena - Pour contenir ces références, une durée de vie
'docpour les données de document a été ajoutée àDocument, séparément de la durée de vie'stringdes chaînes - L’implémentation utilise le crate
typed_arena- Quand une valeur est stockée dans l’arena avec
alloc, une référence vers cette valeur est retournée - Les données allouées peuvent être utilisées tant que l’arena reste en vie
- Quand l’arena sort de portée, les données qu’elle contient sont également supprimées
- Quand une valeur est stockée dans l’arena avec
- La référence retournée par
alloc(&self, value: T) -> &mut Tne peut pas vivre plus longtemps que l’arena, et le borrow checker de Rust empêche les références invalides après la suppression de l’arena
Allouer une seule fois les documents récurrents et les réutiliser
- L’arena sert non seulement à référencer d’autres documents, mais aussi à mettre en cache les
Documentrépétés dans l’ensemble du code - Il n’est plus nécessaire de recréer à chaque fois les documents représentant les mots-clés du langage, comme
String("fn"),String("pub")ouString("type") - Les documents comme
Break { unbroken: ", ", broken: "," }, utilisés entre les éléments d’une liste, sont eux aussi alloués une fois puis réutilisés - Des centaines de petits documents qui devaient auparavant être sans cesse placés dans des
Boxpeuvent désormais être alloués chacun une seule fois
Une API simple, mais de nombreuses modifications des sites d’appel
- L’API de l’arena elle-même est simple, mais convertir une grande partie du code existant a nécessité de nombreuses modifications répétitives
- Les fonctions qui se contentaient auparavant d’appeler
Box::newdoivent désormais recevoir un argument d’arena supplémentaire pour allouer les données - Une fonction comme
format_listpasse désormais la même arena aux sous-fonctions qui formatent les éléments de la liste, et crée les documents imbriqués avecarena.allocau lieu deBox::new - Après plusieurs jours de recherches-remplacements prudents, le travail a été achevé dans une pull request de taille
+2963/-1032 - Ce travail était répétitif et manuel, et aucun LLM n’a été utilisé dans le processus
Amélioration de la vitesse et de l’utilisation maximale de mémoire
- Lors du formatage de
squirrel, un vrai projet Gleam, le temps d’exécution du pretty-printer est passé de 13 ms à 9,8 ms- En comparant uniquement le pretty-printer, cela représente une amélioration des performances de 24 %
- Comme
gleam formatlit et parse aussi le code source du projet en plus du formatage, le pretty-printer ne représente qu’une partie du travail total, mais le temps d’exécution global a tout de même été réduit de 13 % - L’utilisation maximale de mémoire est passée de 8,4 Mo à 7,6 Mo, soit environ 10 % de moins
- En réduisant le nombre d’allocations individuelles sur le tas grâce à l’arena, le formateur s’exécute plus vite tout en consommant moins de mémoire
1 commentaires
Avis sur Lobste.rs
Si vous voulez réduire encore de 10 % le pic d’utilisation mémoire, il peut valoir la peine d’envisager une autre arène qui renvoie des handles opaques au lieu de références. Les handles peuvent être codés sur 16 ou 32 bits plutôt que 64 bits, ce qui est très efficace quand il y a beaucoup de petites allocations
D’après https://donsz.nl/blog/arenas/, le seul crate qui prend cela en charge est
compact_arena, mais si cela ne correspond pas à vos besoins, ce n’est pas non plus difficile à implémenter soi-mêmeOption<T>en les séparant en un indicateur de présence d’à peine plus de 1 bit par élément et les valeursTdes éléments effectivement présentsEn contrepartie, il renvoie
Option<&T>comme type de référence, et non&Option<T>; plus généralement, il s’agit de faire une récursion structurelle en suivant les références, plutôt que de placer des références dans une récursion structurelleLes arènes mériteraient d’être plus largement utilisées, et renvoyer des références plutôt que des handles ou des smart pointers rend le pattern matching sur des énumérations récursives très pratique en Rust. Normalement, même si l’on veut écrire quelque chose comme
if let MyAst::Add(MyAst::Multiply(a, b), c) = expr {}, c’est impossible à cause des boîtes ou pointeurs intermédiaires, mais les références permettent de faire du pattern matching de manière transparente, sans fonctionnalité nightly. Si toutes les références pointent vers une même arène, la durée de vie qui relie tout l’arbre reste également cohérenteCette approche ressemble beaucoup à celle prise en charge par
pretty, une autre bibliothèque de pretty-printing de style Wadler. Cela dit, comme elle prend en charge plusieurs modes d’allocation mémoire, son API peut paraître un peu lourdeIl est surprenant que l’auteur de l’article scientifique cité dans l’article travaille dans une entreprise IT ordinaire à Brunswick