Eden Space
Meine Frage ist also, ob irgendetwas davon wirklich wahr ist, und wenn ja, warum ist Javas Heap-Zuordnung so viel schneller.
Ich habe ein bisschen darüber nachgedacht, wie Java GC funktioniert, da es für mich sehr interessant ist. Ich versuche immer, meine Sammlung von Speicherzuweisungsstrategien in C und C ++ zu erweitern (ich möchte versuchen, etwas Ähnliches in C zu implementieren), und es ist eine sehr, sehr schnelle Möglichkeit, viele Objekte in einem Burst von a zuzuweisen praktische Perspektive, aber vor allem durch Multithreading.
Bei der Java GC-Zuweisung wird eine äußerst kostengünstige Zuweisungsstrategie verwendet, um Objekte zunächst dem Bereich "Eden" zuzuweisen. Soweit ich weiß, wird ein sequentieller Pool-Allokator verwendet.
Das ist viel schneller, nur was den Algorithmus und die Reduzierung von obligatorischen Seitenfehlern angeht, als dies malloc
in C für allgemeine Zwecke oder operator new
in C ++ für Standardzwecke der Fall ist.
Sequenzielle Allokatoren haben jedoch eine große Schwäche: Sie können Chunks mit variabler Größe zuweisen, aber sie können keine einzelnen Chunks freigeben. Sie ordnen nur gerade nacheinander mit Auffüllen für die Ausrichtung zu und können nur den gesamten von ihnen zugewiesenen Speicher auf einmal löschen. Sie sind in der Regel in C und C ++ nützlich, um Datenstrukturen zu erstellen, die nur Einfügungen und keine Entfernung von Elementen erfordern, z. B. einen Suchbaum, der nur einmal beim Start eines Programms erstellt werden muss und dann wiederholt durchsucht wird oder nur neue Schlüssel hinzugefügt werden ( keine Schlüssel entfernt).
Sie können auch für Datenstrukturen verwendet werden, mit denen Elemente entfernt werden können. Diese Elemente werden jedoch nicht aus dem Arbeitsspeicher freigegeben, da sie nicht einzeln freigegeben werden können. Solch eine Struktur, die einen sequentiellen Allokator verwendet, würde immer mehr Speicher verbrauchen, es sei denn , die Daten wurden mit einem separaten sequentiellen Allokator in eine neue, komprimierte Kopie kopiert. Dies ist manchmal eine sehr effektive Technik, wenn ein fester Allokator gewinnt Das ist aus irgendeinem Grund nicht möglich. Ordnen Sie einfach nacheinander eine neue Kopie der Datenstruktur zu und sichern Sie den gesamten Speicher der alten.
Sammlung
Wie im obigen Beispiel für Datenstruktur / sequentiellen Pool wäre es ein großes Problem, wenn Java GC nur auf diese Weise allokiert würde, obwohl es für eine Burst-Allokation vieler einzelner Chunks superschnell ist. Es wäre nicht in der Lage, irgendetwas freizugeben, bis die Software heruntergefahren wird. Zu diesem Zeitpunkt könnte es alle Speicherpools auf einmal freisetzen (bereinigen).
Stattdessen wird nach einem einzelnen GC-Zyklus ein Durchlauf durch vorhandene Objekte im "Eden" -Raum durchgeführt (sequentiell zugewiesen), und diejenigen, auf die noch verwiesen wird, werden dann mithilfe eines Allokators für allgemeine Zwecke zugewiesen, der einzelne Blöcke freigeben kann. Diejenigen, auf die nicht mehr verwiesen wird, werden beim Löschen einfach freigegeben. Im Grunde ist es also "Objekte aus dem Eden-Raum kopieren, wenn sie noch referenziert sind, und dann löschen".
Dies wäre normalerweise ziemlich teuer, daher wird es in einem separaten Hintergrundthread ausgeführt, um zu vermeiden, dass der Thread, der ursprünglich den gesamten Speicher zugewiesen hat, erheblich blockiert.
Sobald der Speicher aus dem Eden-Speicher kopiert und mithilfe dieses teureren Schemas zugewiesen wurde, mit dem einzelne Blöcke nach einem anfänglichen GC-Zyklus freigegeben werden können, werden die Objekte in einen beständigeren Speicherbereich verschoben. Diese einzelnen Chunks werden dann in nachfolgenden GC-Zyklen freigegeben, wenn sie nicht mehr referenziert werden.
Geschwindigkeit
Der Grund, warum der Java GC C oder C ++ bei der direkten Heap-Zuweisung sehr gut übertreffen könnte, ist, dass er die billigste, vollständig entartete Zuweisungsstrategie im Thread verwendet, der die Zuweisung von Speicher anfordert. Dann spart es die teurere Arbeit, die normalerweise bei Verwendung eines allgemeineren Allokators wie Straight-Up erforderlich istmalloc
für einen anderen Thread verwenden würden.
Konzeptionell muss der GC also insgesamt mehr Arbeit leisten, verteilt diese jedoch auf mehrere Threads, sodass die vollen Kosten nicht von einem einzelnen Thread im Voraus bezahlt werden. Dies ermöglicht es dem Thread, Speicher zuzuweisen, es supergünstig zu machen und dann die wahren Kosten aufzuschieben, die erforderlich sind, um die Dinge richtig zu machen, so dass einzelne Objekte tatsächlich zu einem anderen Thread freigegeben werden können. Wenn wir in C oder C ++ malloc
anrufen operator new
, müssen wir die vollen Kosten im Voraus innerhalb desselben Threads bezahlen.
Dies ist der Hauptunterschied, und warum Java C oder C ++ sehr gut übertreffen kann, wenn es nur naive Aufrufe für malloc
oder operator new
zum Zuweisen einer Gruppe von Teeny Chunks verwendet. Natürlich wird es in der Regel einige atomare Operationen und eine mögliche Blockierung geben, wenn der GC-Zyklus einsetzt, aber wahrscheinlich ist er ziemlich optimiert.
Grundsätzlich läuft die einfache Erklärung darauf hinaus, dass in einem einzelnen Thread höhere Kosten anfallen ( malloc
) und in einem anderen Thread niedrigere Kosten anfallen und dann die höheren Kosten anfallen, die parallel ausgeführt werden können ( GC
). Als Nachteil bedeutet dies, dass Sie zwei Indirektionen benötigen, um von der Objektreferenz zur Objektreferenz zu gelangen, damit der Allokator den Speicher kopieren / verschieben kann, ohne vorhandene Objektreferenzen ungültig zu machen. Außerdem können Sie die räumliche Lokalität verlieren, sobald der Objektspeicher leer ist aus dem "Eden" -Raum gezogen.
Last but not least ist der Vergleich etwas unfair, da C ++ - Code normalerweise keine Bootsladung von Objekten einzeln auf dem Heap zuweist. Ordentlicher C ++ - Code neigt dazu, Speicher für viele Elemente in zusammenhängenden Blöcken oder auf dem Stapel zu reservieren. Wenn es eine Schiffsladung winziger Objekte nacheinander in den freien Laden legt, ist der Code beschissen.