Garbage Collection für Systemprogrammierer (2023)
(bitbashing.io)- Selbst in Code, bei dem Performance und Nebenläufigkeit entscheidend sind, wie in Kerneln und Treibern, endet Speicherfreigabe nicht mit einem einfachen Aufruf von
free(); es können lock-freie Verfahren zum Teilen notwendig werden - RCU (Read, Copy, Update) kopiert Daten, die häufig gelesen und selten geändert werden, und ersetzt dann den Pointer atomar, sodass der Lesepfad nicht blockiert wird
- Wenn man die alte Version sofort mit
deletefreigibt, kann in Threads, die noch lesen, ein use-after-free auftreten; daher verfolgt RCU Leseabschnitte und verzögert die Freigabe bis zu einem sicheren Zeitpunkt - RCU wird in Linux zehntausendfach verwendet, existiert auch in der Folly-C++-Bibliothek und in Rusts
crossbeam-epochund hat, weil später je nach Nutzung aufgeräumt wird, eine Form, die GC ähnelt - Die Dichotomie, dass manuelle Speicherverwaltung immer schneller und besser vorhersagbar sei, ist schwach; auch
free(), Referenzzählung und das Speicherverhalten des OS haben Kosten und Unsicherheiten, weshalb moderne GC ebenfalls ein Werkzeug der Systemprogrammierung sein kann
Warum Kernel-Code Verfahren wie RCU nutzt
- Betriebssysteme gehören unter den Programmen, die täglich ausgeführt werden, zu den Bereichen mit besonders hoher Performance-Sensibilität
- Wenn das OS schneller wird, können Nutzer mehr Rechenarbeit erledigen; deshalb investieren Kernel- und Treiberentwickler viel Aufwand in die Optimierung ihres Codes
- Betriebssysteme müssen nicht nur Prozesse und Threads im User Space behandeln, sondern auch verschiedene Threads im Kernel selbst sowie Hardware-Interrupt-Handler
- Weil höhere Latenz den Nutzern Zeit raubt, sind im Kernel-Code verschiedene Verfahren entstanden, um Daten zwischen Threads ohne Locks zu teilen
Grundlegende Funktionsweise von RCU
- RCU (Read, Copy, Update) ist für Daten geeignet, die sehr häufig gelesen, aber selten geschrieben werden
- Ein Beispiel ist die Menge aktuell angeschlossener USB-Geräte: Sie ändert sich kaum, kann sich aber ändern
- Änderungen müssen atomar erfolgen und dürfen Leser, die bereits lesen, nicht blockieren
- Der Schreiber aktualisiert den gemeinsam genutzten Zustand in der folgenden Reihenfolge
- Er liest die vorhandenen Daten aus dem Pointer
- Er kopiert die vorhandenen Daten und wendet die nötigen Änderungen an, um eine neue Version zu erzeugen
- Er aktualisiert den Pointer atomar, sodass er auf die neue Version zeigt
- Leser lesen nur den gemeinsamen Pointer; daher ist der Lesepfad einfach und arbeitet ohne Warten
- Dieser Ansatz ist leicht zu verwenden und wait-free, aber wenn alte Versionen nicht bereinigt werden, entsteht ein Speicherleck
Alte Versionen, die nicht sofort freigegeben werden können
- Wenn man den Pointer direkt nach dem Umschalten auf die neue Version sofort mit
deletefreigibt, entsteht das Risiko eines use-after-free - Da alles ohne Locks läuft, kann der Schreiber nicht wissen, ob es noch Leser gibt, die die alte Version verwenden
- Leser können ihren kritischen Leseabschnitt mit
rcu_read_lock()undrcu_read_unlock()markieren- Leser werden weiterhin nicht blockiert
- Der Schreiber entfernt die alten Daten nicht, bis diese Leser den Abschnitt verlassen haben
rcu_synchronize()muss nicht warten, bis alle Leser verschwunden sind, sondern nur bis jene früheren Leser fertig sind, die die alte Version noch gesehen haben könnten- Leser, die den neuen Pointer gesehen haben, verwenden die neue Version und sind daher für die Lebensdauer der alten Version irrelevant
Verzögerte Freigabe und die Form von GC
- Auch wenn der Schreiber nicht innerhalb der Update-Funktion wartet, bleibt der Code korrekt, solange die alten Daten irgendwann sicher freigegeben werden
- Ein Verfahren wie
rcu_defer(old)kannoldjederzeit freigeben, nachdem die aktuellen Leser den kritischen Abschnitt verlassen haben - Eine Form, bei der ein dedizierter Thread periodisch alte, nicht mehr referenzierte Versionen bereinigt, ähnelt einer generationalen GC
- RCU ist kein Gedankenexperiment, sondern ein in der Praxis weit verbreitetes Verfahren
- Linux verwendet RCU zehntausendfach
- In Facebooks Folly C++ library wird RCU bereitgestellt
- In Rust wird es unter dem Namen
crossbeam-epochverwendet und bildet die Grundlage beliebter Nebenläufigkeitsbibliotheken
- Wichtiger als die Kategoriedebatte, ob RCU „echte GC“ ist, ist, dass die Struktur GC ähnelt, weil Speicher später abhängig davon bereinigt wird, ob er noch in Benutzung ist
Versteckte Kosten manueller Freigabe
- Die verbreitete Annahme, GC sei von Natur aus weniger effizient als manuelle Speicherverwaltung, wird bei einem Blick auf die Implementierungsdetails schwächer
-
free()ist nicht kostenlos- Allgemeine Speicherallokatoren müssen internen globalen Zustand verwalten, etwa vom Kernel erhaltene Seiten, nach Größen aufgeteilte Buckets und Buckets, die gerade in Benutzung sind
- Wenn mehrere Threads versuchen, den Zustand des Allokators zu sperren, kann Contention entstehen
- Selbst wenn es wie bei jemalloc thread-lokale Pools gibt, ist zusätzlicher Code nötig, um diese zu synchronisieren
-
Auch RAII und Lifetimes beseitigen die Kosten des Allokators nicht
- Lifetimes in Rust oder RAII in C++ helfen bei der Automatisierung und Korrektheit der Speicherfreigabe, beseitigen aber nicht die Komplexität der internen Strukturen des Allokators
- In vielen Szenarien landet man wieder bei
shared_ptroderArc - Dafür sind zusätzliche Metadaten in Form von Referenzzählern nötig, und dieser Wert kann zwischen Cores und Caches hin- und herwandern und so Kosten verursachen
- Außerdem können Zyklen im Liveness-Graphen zu Lecks führen
-
Auch GC kann Optimierungen bieten
- Eine bewegliche, generational arbeitende GC komprimiert den Heap regelmäßig neu
- Allokation wird damit fast zu einem bloßen Erhöhen eines Pointers und kann hohen Durchsatz liefern
- Die Lokalität sequenzieller Allokation verbessert zudem oft die Cache-Performance
Irrtümer über die Kontrolle von Speicherverwaltung
- Viele Entwickler, die gegen GC sind, bauen Soft-Real-Time-Systeme
- Sie wollen möglichst schnelle Ausführung, etwa bei der FPS eines Videospiels oder der Kompressionsleistung eines Streaming-Codecs
- Aber es handelt sich nicht um harte Latenzanforderungen, bei denen das System ausfällt oder Menschen sterben, wenn es gelegentlich 1 Millisekunde länger dauert
- Der Glaube, Programmierer könnten bestimmen, wann Speicherverwaltung stattfindet, ist nicht so einfach
- Das Betriebssystem abstrahiert die Interaktion mit der Hardware
- Linux macht bei einer Speicheranforderung standardmäßig fast nichts und kann Speicher erst dann tatsächlich bereitstellen, wenn man ihn wirklich zu benutzen versucht
- Wenn sich
madvise(), Memory-Mapped I/O und Dateisystem-Cache mischen, gibt es keine einfache Antwort auf die Frage, „was wann alloziert wurde“ - An einem schlechten Tag kann schon ein einfacher Pointer-Zugriff zu Disk-I/O führen
- Auch der Glaube, Programmierer wüssten immer, wann ein guter Zeitpunkt zum Anhalten für Speicherverwaltung ist, ist begrenzt
- Es gibt klare Fälle wie den Ladebildschirm eines Videospiels
- In viel Software ist die einzige Antwort: dann, wenn gerade nichts Wichtigeres ansteht
- Einzelner Code mit
shared_ptrundArckann nicht im Voraus wissen, ob er als letzter Besitzer die Aufräumarbeit übernehmen wird
- Auch die Vorstellung, ein Aufruf von
free()gebe den Speicher sofort an das OS zurück, stimmt nicht immer- Speicher wird vom OS seitenweise alloziert
- Allokatoren halten Seiten oft bis zum Programmende fest, um sie wiederzuverwenden
- Das OS kann Seiten auch per Swap zurückholen
Warum GC als Werkzeug der Systemprogrammierung gelten kann
- Nicht jede Software profitiert von GC
- Aber selbst nahe am Jahr 2024 wird die Diskussion über GC unter Systemprogrammierern leicht von falschen Dichotomien sowie Angst, Unsicherheit und Zweifel überlagert
- Die Vorstellung, Sprachen mit GC seien „offensichtlich“ langsamer als Sprachen mit manueller Speicherverwaltung, ist eher Ideologie als Tatsache
- Selbst in Teams, die lebenswichtige Systeme bauen, gab es Fälle von Sub-Mikrosekunden-Latenz mit GC-Sprachen, in denen auf fast jeder Zeile alloziert wurde
- Wenn ein Teil eines Systems zwingend innerhalb von
nTaktzyklen laufen muss, kann genau dieser Teil in Nicht-GC-Code oder in Hardware ausgelagert werden - GC ist keine Universallösung, aber eines der Werkzeuge im Werkzeugkasten, das man ohne Angst einsetzen kann
1 Kommentare
Hacker-News-Kommentare
Als vielversprechende moderne Technik für parallele Garbage Collection sind MPL bzw. MaPLe und dessen neues Automatic Management of Parallelism einen Blick wert.
Der Kern, für den es den POPL 2024 Distinguished Paper Award und den ACM SIGPLAN Dissertation Award 2023 gab, besteht aus zwei Punkten: a) nachweislich effiziente parallele Garbage Collection auf Basis von Disentanglement, b) nachweislich effiziente automatische Granularitätssteuerung.
[1] MaPLe (MPL): https://github.com/MPLLang/mpl
[2] Automatic Parallelism Management: https://dl.acm.org/doi/10.1145/3632880
Dazu gehört auch das verlinkte Paper, außerdem MLKit, das zu den frühen Nutzern und Wegbereitern regionsbasierter Speicherverwaltung zählte.
Könnte ein solcher Ansatz zum Beispiel die Garbage Collection von Go deutlich schneller machen, oder würde man dabei an Probleme im bestehenden Sprachdesign stoßen?
RCU-Anwendungsfälle sind überzeugend, aber meine Erfahrungen mit Garbage Collection in anderen Situationen waren nicht gut
Dieser Text liest sich weniger wie die Behauptung, dass statische Lebensdauern im Allgemeinen besser seien als dynamische, sondern eher wie die Aussage, dass maßgeschneiderte Lösungen für die Speicherverwaltung die beste Performance liefern können
Ich glaube nicht, dass
free()Speicher an das Betriebssystem zurückgibt, sondern an den Allocator. Das ist viel besser, als ihn an das Betriebssystem zurückzugeben, und Systemaufrufe sind langsam. Allerdings kann man es auch so machen wie mimalloc, wo freigegebener Speicher nicht sofort für das nächstemallocverfügbar ist, sondern nur periodischWenn man 800 Byte allokiert, sofort wieder
freeaufruft, das eine Million Mal wiederholt und die Anzahl eindeutiger Pointer zählt, kamen bei glibc malloc 1 heraus, bei jemalloc 1, bei mimalloc 4 und beim Garbage Collector von Julia 6276762767 Stück, also etwa 48 MiB, sind nicht furchtbar schlecht, verdrängen aber trotzdem den L3-Cache meines Rechners. Mit Garbage Collection ist fast garantiert, dass neue Allokationen nicht aus dem Cache, sondern aus dem RAM kommen, und das ruiniert die Performance von Code mit vielen Allokationen. Es geht nicht nur darum, wie schnell die Speicherverwaltung selbst ist, sondern auch darum, wie schnell man mit dem Speicher arbeiten kann, den sie einem gegeben hat
Ich habe dazu einen Benchmark in Julia gepostet: https://discourse.julialang.org/t/blog-post-rust-vs-julia-in...
malloc/free geben dem tatsächlich verwendeten Arbeitsspeicher die Chance, im Cache heiß zu bleiben, wenn er klein genug ist. Auch Allocators wie mimalloc sind, ähnlich wie kompaktierende Garbage Collection, so entworfen, dass aufeinanderfolgende Allokationen nahe beieinander liegen; die 4 eindeutigen Pointer, die ich bei mimalloc gesehen habe, lagen jeweils 896 Byte auseinander
Hätte ich mehr Erfahrung mit kompaktierender Garbage Collection, wäre ich vielleicht weniger zynisch, aber ich sehe Garbage Collection als eine deutlich komplexere Lösung für sichere Speicherverwaltung als etwas wie Rusts Borrow Checker. Diese Komplexität wird auf Compiler- und Runtime-Entwickler abgewälzt, daher ist es für Nutzer normalerweise in Ordnung, und für nicht performancekritischen Code ist es ein akzeptabler Kompromiss. RAII mit statischen Lebensdauern ist ebenfalls ein vernünftiger Kompromiss für Code, der keinen stärker maßgeschneiderten Ansatz braucht; die Beispiele im Artikel sind eindeutig Fälle, in denen eine maßgeschneiderte Lösung nötig ist
Kompaktierende Garbage Collection nutzt den Cache bei lange laufenden Programmen fast immer besser als malloc, weil Heap-Fragmentierung TLB-Cache-Einträge und freien Platz zwischen Objekten verschwendet. Der Bump Allocator einer kompaktierenden Garbage Collection gibt zwar bei jeder Allokation einen neuen Pointer zurück, weil
freekeinen Speicher zurückfordert, aber diese Allokationen sind sequenziell, und wenn man den Heap weiter verbraucht und nur die neuesten Objekte anfasst, bleiben sie trotzdem im Cache. Die Folgeeffekte von Allocators und Garbage Collectors zu benchmarken ist extrem schwierig, und ich betrachte fast alle solchen synthetischen Benchmarks skeptischWorüber sich Leute Sorgen machen, ist, dass der Prozess, herauszufinden, ob Speicher nicht mehr verwendet wird, ineffizienter und nichtdeterministischer ist, als dem Allocator direkt mitzuteilen, dass man mit der Nutzung einer Ressource fertig ist. Ich habe noch niemanden gesehen, der sich über das bloße Verzögern der Freigabe Sorgen macht
Den gesamten lebenden Satz zu durchlaufen ist selten, und in 30 Jahren wurden Garbage-Collection-Algorithmen so weit verbessert, dass sie fast schon fühlend wirken, aber dieser Satz ignoriert — absichtlich oder nicht — den Punkt, an dem sich die Leute tatsächlich stoßen. Wenn es in einem Service Garbage-Collection-Probleme gibt, fühlt es sich an, als müsse man einen Schamanen rufen, der hier und da Tuning betreibt und hofft, die zornige Seele zurück in die Schattenwelt zu schicken
Wenn man Müll markiert und benachrichtigt wird, sobald er nicht mehr verwendet wird, verschwindet dieser ganze Prozess. Speicherallokation kann bei Garbage Collection sehr schnell sein, aber für einen fairen Vergleich muss man auch die Kosten für Markierung und Kompaktierung amortisiert einbeziehen
Ein weiteres großes Problem ist, dass Garbage Collection für die gleiche Performance im Allgemeinen deutlich mehr Speicher benötigt als manuelle Speicherverwaltung. Außerdem braucht sie zusätzliche CPU, um wiederholt und redundant zu prüfen, ob noch Referenzen existieren, und man muss zusätzliche Speicherkopien für optimistische Kompaktierung in Kauf nehmen
Schließlich kritisiert der Artikel, dass bei unklaren Lebensdauern manuelle Speicherverwaltung wie Rusts Arc/Rc nötig sei, ignoriert aber, dass man in Sprachen mit Garbage Collection finalizern nicht zuverlässig vertrauen kann und deshalb praktisch dieselbe Infrastruktur baut, um externe Ressourcen zu schließen
Diese Debatte wurde in den letzten 20 bis 30 Jahren zur Genüge wiederholt, und dieser Artikel scheint nichts Neues beizutragen, außer berechtigte Bedenken gegenüber Garbage Collection als Meme abzutun. Die Memes sind lustig, also ist das okay, aber die richtige Antwort ist, dass es keine allgemein richtige Antwort gibt. Man sollte das Werkzeug verwenden, das zu den Designbeschränkungen des Systems passt
Andere, länger lebende Allokationen sind per Definition aus Cache-Sicht schwer zu kontrollieren. Lokalität ist einer der großen Vorteile von Garbage Collection, und das einzige Problem, das ich kenne, ist Stop-the-world Mark-and-Sweep. Ich weiß, dass moderne Garbage Collectors Hintergrund-Threads haben, aber meines Wissens gibt es trotzdem Stop-the-world-Ereignisse
free()Speicher an den Allocator zurückgibt, stimmt, aber Speicherfragmentierung in einem lange laufenden Server zu behandeln, macht überhaupt keinen SpaßBesonders die interne Fragmentierung von Pages, die von einem Slab Allocator verwaltet werden; das ist kein häufiges Problem, aber ein schwer zu handhabendes
Glücklicherweise gibt es nicht viele Bereiche, in denen Garbage Collection mit derart passend zurechtgebogenen Beispielen konkurrieren muss
Abgesehen von Spezialfällen, in denen sich sämtlicher Speicher leicht über Arenen handhaben lässt, hat eine gute Tracing Garbage Collection die manuelle Speicherverwaltung beim Durchsatz schon vor langer Zeit überholt, und in jüngerer Zeit sind auch die Auswirkungen auf die Latenz für die meisten Anwendungen hinreichend akzeptabel geworden
ZGC von OpenJDK hat typische Pausenzeiten im zwei- bis dreistelligen Mikrosekundenbereich, überschreitet bei vernünftigen Allokationsraten selbst im Worst Case kaum 1 ms und liegt damit in einer ähnlichen Größenordnung wie vom Betriebssystem verursachte Pausen
Der wirklich wichtige Trade-off ist nur der Speicherverbrauch. Abgesehen von speziellen Nischen — also Fällen, in denen Arenen für alles gut passen und die Worst-Case-Latenz im niedrigen Mikrosekundenbereich liegt — bleibt im Kern nur eine Frage: Läuft meine Anwendung in einer speicherbeschränkten Umgebung, oder lohnt es sich, für weniger RAM-Verbrauch andere Dinge zu opfern?
In solchem Code ist es übertrieben, die Lebensdauer einzelner Objekte zu verfolgen. Am Ende geht es bei Speicherverwaltung um Lebensdauern, und weniger unterschiedliche Lebensdauern sind immer besser als viele unterschiedliche, weil man weniger tun muss — egal ob manuell oder automatisch
Nicht über Objektlebensdauern nachdenken zu müssen, ist sehr bequem; deshalb waren Sprachen mit Garbage Collection erfolgreich, auch wenn gute Garbage Collectors intern ziemlich komplex sind
Außerdem bleibt man von schwer vorhersagbarer Tail Latency und den Auswirkungen verschiedener Ausnahmefälle betroffen
ZGC scheint weniger Pausen als Shenandoah zu haben und pro Pause daher mehr Arbeit zu erledigen, wodurch es etwas bessere Performance zeigt
Wir müssen noch Tests in der Produktionsumgebung durchführen, aber bisher wirkt es so, als seien Garbage-Collection-Pausen mit ZGC — und mit generational ZGC ab Java 21 — im Großen und Ganzen ein gelöstes Problem
Aus der Perspektive von Jai lässt sich Speicherallokation, nach Häufigkeit geordnet, in vier Kategorien einteilen: 1) extrem kurzlebige Dinge, die auf den Funktions-Stack gelegt werden können, 2) kurzlebige Dinge mit klar definierter Lebensdauer, die in eine Speicherarena pro Frame/Request passen, 3) langlebige Dinge mit klar definiertem Owner, die über einen Pool eines bestimmten Subsystems verwaltet werden können, 4) langlebige Dinge mit unklarem Owner, die dynamische Speicherverwaltung benötigen
Wer behaupten will, dass Tracing Garbage Collection im Allgemeinen der manuellen Speicherverwaltung überlegen sei, sollte nicht mit Systemen vergleichen, die hier und da malloc/free aufrufen, sondern mit Systemen, die mit dieser Sichtweise im Kopf geschrieben wurden. Ein Vergleich mit modernen C++-/Rust-Praktiken wäre fairer
Ich stimme zu, dass es in den meisten Systemen wahrscheinlich deutlich praktischer ist, sich auf Tracing Garbage Collection zu verlassen, aber das ist eine ganz andere Behauptung
Der Artikel motiviert zunächst RCU und macht dann eine Kehrtwende, um allgemein für universelle Garbage Collection zu argumentieren
Kein trojanisches Pferd, aber es fühlt sich wie ein ziemlich abrupter Wechsel an
Ein Objekt ist in einem von drei Zuständen und wechselt so schnell wie möglich: active, obsolete but alive for old readers, deallocated
Je nachdem, wie der Code geschrieben ist, könnte man „obsolete-but-alive“-Objekte wohl sicher für „new“-Allokationen wiederverwenden, aber die Performance habe ich nicht vollständig analysiert
Wie so oft in Diskussionen über Garbage Collection ist sehr unklar, wann man auf
shared_ptr/Arc„zurückfallen“ muss. In der Praxis ist das Vermeiden von Reference Counting — also zu beweisen, dass bereits Ownership besteht, oder indirekte Referenzen ganz zu vermeiden — der Kern ernsthafter reference-counting-basierter Systeme. Gar nichts zu tun ist natürlich besser als das „irgendwann irgendetwas tun“ der Garbage CollectionIn der Software, die ich schreibe, gibt es zwei Fälle: (1) Hot Paths, in denen man immer maßgeschneiderte Allocators verwendet und Allokationen vermeidet, (2) alles andere
In (1) macht es keinen Unterschied, ob es Garbage Collection gibt oder nicht, und ich werde ausweichen. In (2) ist Garbage Collection wirklich bequem und korrekt
Was Java mit modernen Garbage Collectors geleistet hat, ist beeindruckend, aber selbst dort erkennt man mit Valhalla indirekt an, dass es Platz für allokationsfreien bzw. allokationsarmen Code gibt
Die Beobachtung, dass moderne Betriebssysteme für Nutzer, also keine speziellen RTOS, eine eingebaute Garbage Collection haben, geht hier ein wenig am Thema vorbei
Wir nennen es nur nicht so, sondern Speicherverwaltung. Wie nennt man Sprachen mit eingebauter Garbage Collection? Speicherverwaltete Sprachen
In alten C-Programmen, die „von oben nach unten“ ausgeführt werden, sieht man das oft. Sie allokieren und räumen Systemressourcen auf, kümmern sich aber nicht um
free. Wenn das Programm endet, holt sich das Betriebssystem den gesamten Speicher zurück – warum sollte man es also tun?Hier gäbe es die Chance, einen Garbage Collector auf Betriebssystemebene zu bauen, der weniger stark vom Programm isoliert ist, und ein Betriebssystem, das Ressourcen ähnlich behandelt wie der Garbage Collector einer Sprach-Laufzeitumgebung. Aber in Garbage-Collection-Sprachen ist der Garbage Collector normalerweise komplex mit fast jeder Zeile der Laufzeitumgebung verflochten; daher ist es unpraktisch, nur eine Distribution für ein Betriebssystem so zu bauen, dass sie diese Kontrolle an das Betriebssystem abgibt
Trotzdem ist es schade. Denn es gibt viel Spielraum, chronische Probleme zu verbessern, die durch die künstliche Trennung zwischen Speicherverwaltung auf Programmebene und Speicherverwaltung auf Betriebssystemebene entstehen
Der einzige Grund, Speicher freizugeben, ist, ihn in langlebigen Anwendungen für andere Allokationen wiederzuverwenden, ohne vom Betriebssystem neuen Speicher zu holen. Bei Kommandozeilen-Tools, die einmal laufen und dann enden, ist das meist nicht nötig
Wenn ein Prozess so entworfen ist, dass er endet, sobald seine Arbeit erledigt ist, kann man das Betriebssystem wie einen Garbage Collector nutzen
Es gab jedoch nie eine Möglichkeit, mit der das Betriebssystem erkennen könnte, welcher Speicher innerhalb eines laufenden Programms nicht mehr verwendet wird. Allenfalls obskure Forschungsbetriebssysteme könnten eine Ausnahme sein. Daher sehe ich das weniger als verpasste Chance, sondern eher so, dass das angeblich Verpasste in sinnvoller Form gar nicht existiert
Andererseits ist ein Programmierstil mit sehr einfachen, kurzlebigen Programmen völlig legitim. CLI-Tools und Skriptsprachen, die sie skripten, funktionieren so; frühere Webserver funktionierten mit CGI usw. ebenfalls so, und auch heute ist das ein durchaus vernünftiger Ansatz
(1) Der Übergang von RCU zu allgemeiner Tracing-Garbage-Collection wirkt wie ein Bait-and-Switch
(2) Manuelle Speicherverwaltung besteht nicht nur aus malloc/free-Aufrufen, sondern auch aus Layout. Beispiele sind das Auftrennen von Arrays von Structs, Inlining, implizite Offsets, Packing usw.
Gemeint ist offenbar „Array von Structs“; das lässt sich mit Tuple-Arrays erreichen und wird je nach Target natürlich abgeflacht bzw. normalisiert. Auf nativen Targets wird es also zu einem Array von Structs
Man kann auch bytegenaue Layouts definieren[1], was hauptsächlich für die Interaktion mit anderer Software oder das Parsen binärer Formate genutzt wird. Algebraische Datentypen können unboxed werden, und bald wird sich sogar die exakte Codierung algebraischer Datentypen steuern lassen
Virgil verwendet Garbage Collection
[1] https://github.com/titzer/virgil/blob/master/doc/tutorial/La...
Es stimmt zwar, dass modernen Managed Languages Kontrolle über das Speicherlayout fehlt, aber auch Low-Level-Sprachen sind weit von perfekt entfernt, und es gibt durchaus Möglichkeiten, Einfluss darauf zu nehmen
Was in diesem Artikel fehlt, ist der Punkt, dass async/await sehr gut zu Garbage Collection passt
Aus persönlichen, etwas eigenwilligen Stilgründen mag ich async/await nicht, aber ich will das nicht weiter ausführen
Ich habe es viel in TypeScript/JavaScript genutzt und auch in Dart; dort funktioniert es so, wie man es erwartet
In Rust habe ich es ebenfalls verwendet, und meiner Meinung nach ist es eine Katastrophe. Die Art von Speicherverwaltung, die nötig ist, um async/await in einer Multithread-Runtime zu verwenden, hineinzuzwingen, ist die Hölle
https://doc.rust-lang.org/std/pin/index.html
In vielen Texten, die Garbage Collection verteidigen, fehlt ein Punkt, und auch dieser Artikel wirkt so: Speicher ist nur eine Art von Ressource.
Gerade in der Systemprogrammierung muss korrekter Code auch externe Ressourcen wie File-Handles, Sockets usw. verwalten. Garbage Collection löst nur den Speicherteil einer Anwendung und hilft bei solchen externen Ressourcen überhaupt nicht. Im Gegenteil, sie kann es deutlich komplizierter machen; man muss sich nur ansehen, was nötig ist, um in .NET ein nichttriviales
IDisposablekorrekt zu implementieren.Ansätze wie RAII oder Reference Counting machen es meiner Erfahrung nach viel einfacher, Speicher und externe Ressourcen einheitlich zu behandeln, wodurch es auch leichter wird, korrekten Code zu schreiben und über ihn nachzudenken.
Das heißt nicht, dass ich Garbage Collection grundsätzlich ablehne. Wie alles andere ist sie ein Werkzeug mit Vor- und Nachteilen. Der im Artikel erwähnte „manuelle GC“-Ansatz mit RCU ist für bestimmte Aufgaben interessant.
Es ist kein Zufall, dass die meisten theoretischen Berechnungsmodelle unendlichen Speicher annehmen. In mancher Software, etwa Betriebssystem-Kernels oder harten Echtzeitanwendungen, wird auch Rechenleistung manuell zugewiesen, aber es gibt kaum Sprachen, die verlangen, Rechenleistung manuell zu allokieren.
Aus ähnlichen Gründen ist automatische Speicherverwaltung sehr nützlich, um Berechnung zu abstrahieren. Sie verhindert, dass Speicher-Details einer Subroutine zum Aufrufer durchsickern; bei der CPU-Nutzung treten solche Details ebenfalls nur selten zutage.
Jede nichttriviale Berechnung umfasst eine gewisse nichtkonstante Menge an Verarbeitung und Speicher, während I/O normalerweise am Rand des Systems stattfindet. I/O-Verwaltung ist natürlich ebenfalls sehr wichtig, aber für den Begriff der Berechnung und die Zentralität von Berechnungsabstraktionen ist sie nicht so fundamental wie Verarbeitung und Speicher.
Rust hat zum Beispiel am Ende I/O-Sicherheit bekommen, sodass File-Handles wie Unix’
OwnedFdoder Handles wie Windows’OwnedHandlezu besessenen Objekten wurden und nicht einfach zu Integern wie der Zahl 4.Oberflächlich betrachtet scheint es darum zu gehen, dumme Fehler zu vermeiden, etwa Arithmetik auf Handles auszuführen oder reservierte Werte fälschlich als Sentinel zu verwenden. Durch das Ownership-Modell entsteht aber selbst bei kniffligen Arbeiten mit Handles explizite Ownership, was auch für spätere Maintainer transparent bleibt.
Den Wert von RAII habe ich wirklich stark zu schätzen gelernt.
Manche Sprachen haben RAII, manche stellen Keywords bereit, manche bieten arenaartige Verwaltung oder Lambdas mit impliziter Verwaltung. Manche Sprachen bekommen etwas Hilfe vom Typsystem, andere mischen von alledem ein bisschen.
Außerdem können statische Analysatoren in solchen Sprachen, so wie Systementwickler auf statische Analysatoren angewiesen sind, Versäumnisse prüfen, wenn das Typsystem allein nicht ausreicht.
Zum Beispiel garantiert Javas try-with-resources-Statement, dass Ressourcen auch bei einer Ausnahme sicher freigegeben werden: https://docs.oracle.com/javase/tutorial/essential/exceptions...
Schon mit solchen grundlegenden Bausteinen lassen sich ziemlich robuste und ressourcensichere Systeme bauen.