Die Effizienz von Garbage Collectors (GC) ist für die Performance vieler Programmiersprachen und Laufzeitumgebungen entscheidend. Besonders interessant ist hierbei die Geschwindigkeit, mit der der GC neue Objekte im Speicher anlegen kann. RPython, eine statisch typisierte Teilmenge von Python, die vor allem für die Implementierung der PyPy-Laufzeitumgebung entwickelt wurde, verfügt über einen eigenen Garbage Collector. Die Frage, wie schnell dieser GC Objekte zuweisen kann, ist sowohl für Entwickler als auch Forscher relevant, die die Speicherverwaltung und damit die Gesamtsystemperformance optimieren möchten. RPython-Objektzuweisung in der Praxis: Ein Benchmark-Ansatz Um die Geschwindigkeit der Objektzuweisung des RPython Garbage Collectors präzise zu bestimmen, wurde ein eigens entwickeltes Benchmark-Programm verwendet.
Die Idee dahinter ist denkbar einfach: In einem so genannten Tight Loop wird immer wieder ein neues Objekt der Klasse A instanziiert. Dabei wird sichergestellt, dass jeweils mindestens zwei Objekte gleichzeitig am Leben bleiben, um Optimierungen durch den Compiler wie Escape Analysis zu vermeiden, die mögliche Speicherzuweisungen entfernen könnten. Dieses Vorgehen garantiert, dass der Garbage Collector tatsächlich jedes Objekt anlegt, anstatt die Zuweisung durch Optimierungen vorab zu eliminieren. Auf einer 64-Bit-Architektur benötigt ein Objekt der Klasse A aufgrund von RPython-spezifischen Meta-Informationen und Feldgrößen etwa 16 Bytes Speicherplatz. Dadurch lassen sich Abschätzungen zur tatsächlich zugewiesenen Speichermenge relativ leicht treffen und mit den Laufzeitmessungen in Zusammenhang bringen.
Sowohl Benchmarks mit initialisierten Feldern als auch ohne Initialisierung der Objektattribute wurden durchgeführt, um den Einfluss der zusätzlichen Speicherzugriffe beim Initialisieren zu evaluieren. Die Ergebnisse machen deutlich, dass die reine Zuordnung ohne Initialisierung messbar schneller ist, obwohl der Unterschied moderat bleibt, da die Zuweisung an sich der dominierende Faktor ist. Vergleich mit dem Boehm Garbage Collector Ein interessanter Aspekt der Untersuchung ist der Vergleich mit dem weit verbreiteten Boehm GC, der häufig in C/C++-Programmen zum Einsatz kommt. Der Boehm GC verwendet conservative stack scanning, was unter anderem bedeutet, dass der Speicher nicht beweglich ist, was die Komplexität der Allokation erhöht. In den durchgeführten Tests zeigte der RPython GC eine deutlich bessere Performance als der Boehm GC, was auf seinen designbedingten Vorteil zurückzuführen ist.
Auf einem modernen AMD Ryzen 7 PRO 7840U erreichte der RPython GC bei 1 Milliarde Objektzuweisungen ohne Initialisierung eine Geschwindigkeit von über 34 GB/s zugewiesenem Speicher, wohingegen der Boehm GC trotz selbem Test nur knapp über 1,5 GB/s erzielte. Selbst mit Initialisierung der Objekte blieb der RPython GC mit rund 30 GB/s an zweiter Stelle, der Boehm GC hingegen verharrte bei ähnlichem Wert. Mechanismus der Allokation: Bump-Pointer-Strategie Die Effizienz des RPython GCs erklärt sich auch durch die Art seiner Allocationslogik. Im sogenannten fast path nutzt der GC eine Bump-Pointer-Strategie. Dabei wird ein Zeiger auf den nächsten freien Bereich im sogenannten Nursery-Speicherbereich verschoben, um Platz für das neu anzulegende Objekt zu schaffen.
Diese Strategie ist außerordentlich performant, da sie ohne komplexe Suchalgorithmen oder Verwaltungsdatenstrukturen auskommt, solange ausreichend Platz im Nursery vorhanden ist. Erreicht der GC dagegen das Ende des freien Bereichs, also wenn keine weitere Allokation im aktuellen Nursery möglich ist, wird eine Minor Collection ausgelöst. Diese sammelt nicht mehr benötigte Objekte ein und schafft Platz für neue Zuweisungen. Die Kosten einer solchen Minor Collection sind im Vergleich zur Allokation relativ gering, da sie hauptsächlich in Abhängigkeit von den überlebenden Objekten arbeitet. In den Benchmark-Tests zeigte sich, dass dieser Aufwand nur etwa zwei Prozent der Gesamtlaufzeit der Schleife beansprucht, was die Effizienz der GC-Implementierung nochmals unterstreicht.
Technische Details der CPU-Leistung und Instruktionsanalyse Eine tiefere Betrachtung der Hardware-nahen Ausführung offenbart eindrucksvolle Werte: Unter Verwendung von Performance-Analyse-Tools wie perf wurde festgestellt, dass die CPU-Auslastung über fünf Instruktionen pro Zyklus im Allokationsloop beträgt. Die Anzahl der CPU-Zyklen pro Allokation liegt bei knapp über zwei, was eine extrem schnelle Umsetzung des Allokationscodes signalisiert. Der maschinennahe Code selbst verdeutlicht den minimalistischen und direkt zielgerichteten Charakter der Allokation: Laden des aktuellen Freizeigers im Nursery, Verschiebung um die Objektgröße, Abgleich mit der oberen Grenze des Nursery, und gegebenenfalls Auslösen der Minor Collection. Diese kurzen und optimierten Maschinencode-Folgen sorgen für die hohe Geschwindigkeit. RPython GC im Vergleich zur PyPy-JIT-Allokation Ein spannender Perspektivwechsel ergibt sich, wenn der gleiche Allokationscode als reguläres Python-Programm innerhalb der PyPy-Laufzeitumgebung ausgeführt wird.
Hierbei müssen neben den Interpreter-Overheads auch die dynamischeren Typmuster der Python-Objekte berücksichtigt werden. Objekte, gerade benutzerdefinierte Instanzen in normalem Python, sind viel umfangreicher und benötigen deutlich mehr als die 16 Bytes der RPython-Objekte. Dennoch zeigt ein Benchmark mit einfachen Integer-Zuweisungen, dass die PyPy JIT-Variante auch hier gute Werte erzielt. Bei deaktiviertem JIT fällt die Geschwindigkeit allerdings drastisch ab, was einmal mehr die Bedeutung von JIT-Kompilierung und statischer Typinformation bei der Optimierung von Speicheroperationen verdeutlicht. Bedeutung der Cache-Architektur und der Speicherhierarchie Die RPython GC-Konstruktion nutzt gezielt die Größe moderner Level-2 (L2) CPU-Caches für die Dimensionierung des Nursery-Speichers.
Mit einer typischen Größe von 4 MiB passt das Nursery komplett in den schnellen L2-Cache, was die Zugriffszeiten erheblich reduziert und die hohe Zuweisungsgeschwindigkeit erst ermöglicht. Die Tests bestätigen, dass trotz riesiger Objektmengen im Gigabyte-Bereich die schnelle Arbeit im Cache erfolgt. Die niedrige Rate an Cache-Misses demonstriert, wie effizient der Memory-Footprint und die Allokationsstrategie auf die Hardwareressourcen abgestimmt sind. Fazit Die Analyse der RPython Garbage Collector-Performance zeigt, dass gut gestaltete Speicherzuweisungen auf Basis von einfachen aber effektiven Strategien wie dem Bump-Pointer-Allocator sehr hohe Allokationsraten erreichen können. Der Vergleich zu existierenden Systemen wie dem Boehm GC unterstreicht das Potenzial und die Vorteile des RPython-Ansatzes.
Darüber hinaus machen die Benchmarks und maschinennahe Codeanalyse deutlich, wie sehr moderne CPUs und deren Cache-Architektur zur Effizienz solcher Speicheroperationen beitragen. Insgesamt ist der RPython GC in der Lage, mehrere zehn Gigabyte pro Sekunde an Objekten zuzuweisen - eine beeindruckende Leistung, die zeigt, dass sorgfältiges Systemdesign in Kombination mit moderner Hardware bei der Speicherverwaltung große Leistungssprünge ermöglicht. Für Entwickler und Forscher ist das Wissen um die Details des RPython GCs nicht nur theoretisch interessant, sondern kann auch bei der Optimierung eigener Systeme und Laufzeitumgebungen von großem Nutzen sein. Die Kombination aus schneller Allokation, geringer GC-Overheadzeit und der Kompaktheit der Objekte macht RPython weiterhin zu einer optimalen Basis für hochperformante interpreterspezifische Softwarelösungen.