Ansicht umschalten
Avatar von
  • unbekannter Benutzer

mehr als 1000 Beiträge seit 14.01.2016

Wenn ich schon "Overhead einer Speicherverwaltung" lese

Diese Märchen ist offenbar nicht totzukriegen. Automatische Speicherverwaltungen ("GC") haben im Schnitt _weniger_ Overhead als manuelle Speicherverwaltung per malloc/free.

Das lässt sich sogar relativ einfach testen.
Einfach free() durch etwas ersetzen, das gar nichts tut.
Und malloc() durch etwas, das einen großen statischen Speicherblock nimmt und einfach immer die ersten N Bytes zurückgibt, dass kann man durch einen einfachen inkrementierenden Zeiger implementieren.
Das Programm wird irgendwann wegen Speichermangel abbrechen, aber bis zu diesem Punkt könnt ihr Performance-Tests machen und vergleichen mit der, was im Normalbetrieb rauskommt. Evtl. auch mitzählen, wie viele malloc()-Aufrufe das Programm macht, um die Zahlen einordnen zu könnnen.
Ich hab das Experiment nicht selbst gemacht, aber etliche Papers von Leuten gelesen, die das getan haben. Und die hatten sehr erstaunliche Zahlen zu vermelden. (Was sie nicht getan haben: Mal richtig große Applikationen damit vergleichen. Wenn jemand ein Setup hat, in dem er sowas selber compiliert, wär's mal interessant, hier was zu lesen.)

Die Zusammenhänge sehen ungefähr so aus:

1. Mit malloc/free muss man immer mitführen, welches Stück Programm eigentlich fürs Freigeben zuständig ist. Das ist eine zusätzliche Randbedingung für die Softwarearchitektur, was angesichts der häufig zahlreichen anderen Randbedingungen bedeutet, dass man woanders Abstriche hinnehmen muss, evtl. auch bei Performance.
2. Wenn man mit Daten hantiert, die von einer Library erzeugt und auch wieder vernichtet werden, man die Daten aber später noch braucht, muss man defensive Sicherheitskopien anlegen, was auch Speicher- _und_ Laufzeitperformance schlägt.

Die Punkte oben kann man mit dem Test nicht messen, ihre Relevanz wird daher gern rein ideologisch beurteilt.
Ich weiß nur, dass sie real sind, Punkt 1 dürfte ziemlich verbreitet, aber mäßig wirksam sein, Punkt 2 eher selten, beide Punkte bewegen sich je nach Softwareprojekt zwischen "vernachlässigbar" und "katastrophal". Voraussagen sind schwierig (was schlecht ist).

3. Ein effizientes(!) malloc/free benötigt eine sehr komplizierte Freispeicherverwaltung. Freie Blöcke müssen mit Anfang und Ende verwaltet werden, benachbarte freie Blöcke müssen identifiziert und verschmolzen werden.
Das sind recht komplizierte Datenstrukturen und eine Menge Schreibzugriffe im RAM, insbesondere free() kann recht teuer werden.
4. Will man die Punkte 1 und 2 loswerden, greift der C-Programmierer zu Reference Counting. Allerdings verliert man dabei die Kontrolle über den Freigabezeitpunkt: Wird der letzte Zeiger freigegeben, der eine große Datenstruktur aus vielen Speicherblöcken am Leben erhält, kriegst du eine Freigabekaskade. Das kann durchaus zu hässlichen Stockungen führen. Man kann diese Situation auch nicht testen, da man ja eben _nicht_ kontrollieren will, wann freigegeben wird.
5. Freigabekaskaden werden gern dadurch bekämpft, dass man nie mehr als eine gewisse Zahl an Blöcken auf einmal freigibt. Das reduziert allerdings den Vorteil, dass man freiwerdenden Speicher sofort wieder hat.

Automatische Garbage Collection wird heutzutage kopierend gemacht: Die freiwerdenden Blöcke werden überhaupt nicht verwaltet. Irgendwann wird der Heap voll, dann kopiert man die noch erreichbaren Blöcke in einen neuen Heap.
Mit allerlei Tricks (die kompliziert zu erklären sind, ein bisschen CPU-Unterstützung benötigen, aber kaum CPU-Zyklen benötigen) lässt man die GC parallel zum eigentlichen Programm laufen. In diesem Modus kann man den Zeitbedarf der GC stufenlos zwischen "schnelle Freigabe" und "wenig CPU-Last für Freigabe" regeln. Hat man Zeitkritisches, kann man das auf einer höheren Priorität als dem GC-Thread machen und muss sich nicht über Freigabekaskaden sorgen.
Unterm Strich ist die Sache sehr, sehr performant.

Der Haken und warum die meisten Leute trotzdem bei Reference Counting landen: Vom ersten Anlauf bis zur ersten performanten kopierenden GC dauert es gern ein paar Jahre. Die Tricks sind halt nicht nur kompliziert zu erklären, sondern auch kompliziert umzusetzen.
Und da Performancetests umso schwieriger werden, je älter und komplexer die Software ist, hat man meist nur die Zahlen aus der Anfangszeit, und dort ist RC dann tatsächlich schneller.

Es gibt auch für malloc/free allerlei Optimierungen, wie z.B. dass man Speicherblöcke, die die garantiert noch vor Ende der Funktion freigegeben werden, direkt auf dem Heap alloziert.
Das geht mit automatischer GC natürlich auch, allerdings mit dem Compiler. Das ist dann der übliche Trade-Off: Bis der Compiler das kann, dauert's länger, dafür kann er's dann zuverlässiger als der Programmierer.

In alten, gut abgehangenen Runtimes wie .net oder JVM ist die automatische GC meist wirklich schnell. (Dass viele Java-Programme langsam starten, liegt am JIT und an altem JDK-Code, der manche Standardkomponenten sehr langwierig und aufwändig initialisiert. Die GC ist es nicht.)

Bewerten
- +
Ansicht umschalten