Ein 3 Jahre altes Performance-Problem in Gleam mit Rust-Arenas lösen
(giacomocavalieri.me)- Im rekursiven
Document-Aufbau des Gleam-Formatierers wurden die wiederholten einzelnen Heap-Allokationen durch arena-basierte Referenzen ersetzt, wodurch ein seit 3 Jahren offenes Performance-Problem behoben wurde Box<Self>wurde durch in einer Arena gespeicherte&Selfersetzt, und Rusts Lebensdauerprüfung stellt sicher, dass nach dem Freigeben der Arena keine internen Daten mehr referenziert werden- Hunderte wiederkehrende
Document-Objekte wie Sprach-Keywords und Kommas werden nur einmal allokiert und wiederverwendet, wodurch sich die Laufzeit des Pretty-Printers von 13 ms auf 9,8 ms verringerte und er 24 % schneller wurde - Auch die gesamte Laufzeit von
gleam format, einschließlich Einlesen des Quellcodes und Parsing, wurde um 13 % verkürzt, während der maximale Speicherverbrauch von 8,4 MB auf 7,6 MB und damit um etwa 10 % sank - Obwohl für die Weitergabe der Arena umfangreiche Änderungen an Funktionen und Aufrufstellen nötig waren und manuelle Arbeit im Umfang von
+2963/-1032erforderlich war, konnten mit der Reduzierung wiederholter Einzelallokationen sowohl Geschwindigkeit als auch Speicherverbrauch verbessert werden
Allokationskosten in einer rekursiven Document-Struktur
- Der Pretty-Printer von Gleam verwendet eine rekursive
Document-Datenstruktur, die beschreibt, wie Code ausgegeben wird und an welchen Stellen bei Überschreiten der Zeilenbegrenzung umgebrochen werden soll - Zu den wichtigsten Varianten gehören
Stringfür auszugebende Zeichenketten,Breakzur Definition von Zeichenketten vor und nach einem Zeilenumbruch,Groupzum Bündeln mehrerer Dokumente undNestzum Erhöhen der Einrückung bei einem Umbruch - Auch die Liste
[1, 2]wird durch eine Kombination ausString,Break,NestundGroupdargestellt- Passt sie in die aktuelle Zeile, wird sie in einer Zeile als
[1, 2]ausgegeben - Überschreitet sie die Zeilenbegrenzung, wird sie entsprechend dem angegebenen
Breakauf mehrere Zeilen verteilt, eingerückt und mit dem abschließenden Komma ausgegeben
- Passt sie in die aktuelle Zeile, wird sie in einer Zeile als
- Varianten wie
Nest, die ein anderesDocumententhalten, verwendeten bisherBox<Self>, weshalb für jedes verschachtelte Dokument eine einzelne Heap-Allokation nötig war, was einen erheblichen Teil der Laufzeit ausmachen konnte - Der zugrunde liegende Algorithmus des Gleam-Pretty-Printers wird im Paper Strictly Pretty beschrieben
Box durch Arena-Referenzen ersetzen
- Das bisherige
Nest(Box<Self>)wurde zuNest(&'doc Self)geändert, sodass die Struktur nun ein anderes in der Arena gespeichertesDocumentreferenziert - Um Referenzen aufnehmen zu können, wurde
Documentzusätzlich zur String-Lebensdauer'stringum die Lebensdauer'docfür die Dokumentdaten erweitert - Für die Implementierung wurde das Crate
typed_arenaverwendet- Mit
allocwird ein Wert in der Arena gespeichert und eine Referenz auf diesen Wert zurückgegeben - Allokierte Daten können verwendet werden, solange die Arena lebt
- Verlässt die Arena den Gültigkeitsbereich, werden auch die darin enthaltenen Daten entfernt
- Mit
- Die von
alloc(&self, value: T) -> &mut Tzurückgegebene Referenz kann nicht länger leben als die Arena, und Rusts Borrow Checker blockiert ungültige Referenzen nach dem Freigeben der Arena
Wiederkehrende Dokumente einmal allokieren und wiederverwenden
- Die Arena dient nicht nur dazu, andere Dokumente zu referenzieren, sondern wird im gesamten Code auch zum Cachen wiederkehrender
Document-Objekte genutzt - Dokumente wie
String("fn"),String("pub")undString("type"), die Sprach-Keywords darstellen, müssen nicht mehr jedes Mal neu erzeugt werden - Auch Dokumente wie
Break { unbroken: ", ", broken: "," }, die zwischen Listenelementen verwendet werden, werden einmal allokiert und anschließend wiederverwendet - Hunderte kleine Dokumente, die zuvor fortlaufend in
Boxverpackt werden mussten, können nun jeweils nur einmal allokiert werden
Eine einfache API, aber weitreichende Änderungen an den Aufrufstellen
- Die Arena-API selbst ist einfach, doch die Umstellung großer Teile des bestehenden Codes erforderte wiederholte Anpassungen
- Funktionen, die zuvor nur
Box::newaufriefen, müssen nun zusätzlich ein Arena-Argument entgegennehmen, um Daten darin zu allokieren - Funktionen wie
format_listgeben dieselbe Arena nun auch an Unterfunktionen zur Formatierung von Listenelementen weiter und erzeugen verschachtelte Dokumente mitarena.allocstatt mitBox::new - Nach tagelangem vorsichtigen Suchen und Ersetzen wurde die Arbeit in einem Pull Request im Umfang von
+2963/-1032abgeschlossen - Die Arbeit war repetitiv und manuell; ein LLM wurde dabei nicht verwendet
Verbesserungen bei Geschwindigkeit und maximalem Speicherverbrauch
- Beim Formatieren des realen Gleam-Projekts
squirrelsank die Laufzeit des Pretty-Printers von 13 ms auf 9,8 ms- Betrachtet man nur den Pretty-Printer, entspricht das einer Leistungssteigerung von 24 %
- Da
gleam formatneben dem Formatieren auch den Quellcode des Projekts einliest und parst, macht der Pretty-Printer nur einen Teil der gesamten Arbeit aus, dennoch verkürzte sich auch die Gesamtlaufzeit um 13 % - Der maximale Speicherverbrauch sank von 8,4 MB auf 7,6 MB und damit um etwa 10 %
- Durch die Reduzierung einzelner Heap-Allokationen mit Arenas wurde der Formatierer schneller und benötigte zugleich weniger Speicher
1 Kommentare
Lobste.rs-Meinungen
Wenn man die maximale Speichernutzung noch um 10 % senken möchte, lohnt es sich, eine andere Arena in Betracht zu ziehen, die statt Referenzen opake Handles zurückgibt. Wenn sich die Handles statt als 64 Bit als 16 oder 32 Bit darstellen lassen, bringt das viel bei vielen kleinen Allokationen
Laut https://donsz.nl/blog/arenas/ unterstützt das allerdings nur das Crate
compact_arena; wenn es nicht zu den Anforderungen passt, ist eine eigene Implementierung aber auch nicht besonders schwerOption<T>aufgeteilt in ungefähr 1 Bit Präsenzinformation pro Eintrag und dieTder tatsächlich vorhandenen EinträgeStatt einer Referenzform
&Option<T>gibt es dannOption<&T>zurück; allgemeiner gesagt folgt man bei struktureller Rekursion eher den Referenzen, statt Referenzen innerhalb der strukturellen Rekursion zu habenArenen verdienen eine breitere Nutzung, und wenn statt Handles oder Smart Pointern Referenzen zurückgegeben werden, wird Pattern Matching auf rekursiven Enums in Rust sehr bequem. Normalerweise möchte man etwas wie
if let MyAst::Add(MyAst::Multiply(a, b), c) = expr {}schreiben, was wegen Boxen oder Zeigern dazwischen nicht möglich ist, aber mit Referenzen funktioniert transparentes Pattern Matching ohne Nightly-Features. Wenn alle Referenzen auf dieselbe Arena zeigen, bleibt auch die Lifetime, die den gesamten Baum verbindet, konsistentDieser Ansatz ist auch ziemlich ähnlich zu dem, was eine andere Pretty-Printing-Bibliothek im Wadler-Stil,
pretty, unterstützt. Da sie allerdings mehrere Strategien der Speicherallokation unterstützt, kann die API etwas schwergewichtig wirkenÜberraschend, dass der Autor des im Beitrag zitierten Papers bei einer gewöhnlichen IT-Firma in Braunschweig arbeitet