Ich habe diese Frage bereits auf SO gepostet und sie ist in Ordnung. Es wurde leider geschlossen (es braucht nur eine Stimme, um es wieder zu öffnen), aber jemand schlug vor, dass ich es hier posten sollte, da es besser passt. Das Folgende ist also buchstäblich eine Kopie der Frage
Ich habe die Kommentare zu dieser Antwort gelesen und dieses Zitat gesehen.
Objektinstanziierung und objektorientierte Funktionen sind blitzschnell zu verwenden (in vielen Fällen schneller als C ++), da sie von Anfang an entwickelt wurden. und Sammlungen sind schnell. Standard-Java schlägt Standard-C / C ++ in diesem Bereich, auch für die meisten optimierten C-Code.
Ein Benutzer (mit wirklich hohen Wiederholungszahlen, möchte ich hinzufügen) hat diese Behauptung kühn verteidigt und dies behauptet
Die Heap-Zuweisung in Java ist besser als in C ++
und fügte diese Aussage hinzu, die die Sammlungen in Java verteidigt
Und Java-Sammlungen sind im Vergleich zu C ++ - Sammlungen aufgrund der unterschiedlichen Speichersubsysteme schnell.
Meine Frage ist also, ob irgendetwas davon wirklich wahr ist, und wenn ja, warum ist Javas Heap-Allokation so viel schneller.
quelle
Antworten:
Dies ist eine interessante Frage, und die Antwort ist komplex.
Insgesamt kann ich mit Recht sagen, dass der JVM-Garbage Collector sehr gut konzipiert und äußerst effizient ist. Es ist wahrscheinlich das beste Allzweck- Speicherverwaltungssystem.
C ++ kann den JVM-GC mit speziellen Speicherzuordnern schlagen , die für bestimmte Zwecke entwickelt wurden. Beispiele könnten sein:
Spezialisierte Speicherzuordnungen sind natürlich per Definition begrenzt. In der Regel gelten Einschränkungen hinsichtlich des Objektlebenszyklus und / oder des Objekttyps, der verwaltet werden kann. Die Speicherbereinigung ist wesentlich flexibler.
Die Garbage Collection bietet Ihnen auch einige signifikante Vorteile aus Sicht der Leistung:
Java GC hat einen großen Nachteil: Da das Sammeln von Datenmüll in regelmäßigen Abständen verschoben und in Stücken erledigt wird, werden bei gelegentlichen GC-Pausen Datenmüll gesammelt, was die Latenz beeinträchtigen kann. Dies ist normalerweise kein Problem für typische Anwendungen, kann jedoch Java in Situationen ausschließen, in denen harte Echtzeit erforderlich ist (z. B. Robotersteuerung). Weiche Echtzeit (z. B. Spiele, Multimedia) ist normalerweise in Ordnung.
quelle
Dies ist keine wissenschaftliche Behauptung. Ich gebe nur ein paar Denkanstöße zu diesem Thema.
Eine visuelle Analogie ist folgende: Sie erhalten eine Wohnung (eine Wohneinheit), die mit Teppichboden ausgelegt ist. Der Teppich ist schmutzig. Was ist der schnellste Weg (in Stunden), um den Boden der Wohnung blitzsauber zu machen?
Antwort: Rollen Sie einfach den alten Teppich auf. wegschmeißen; und einen neuen Teppich ausrollen.
Was vernachlässigen wir hier?
Die Speicherbereinigung ist ein großes Thema, und sowohl in Programmers.SE als auch in StackOverflow gibt es viele Fragen.
Ein C / C ++ - Zuordnungsmanager namens TCMalloc zusammen mit der Objektreferenzzählung kann theoretisch die besten Leistungsansprüche aller GC-Systeme erfüllen.
quelle
Der Hauptgrund ist, dass Java, wenn Sie nach einem neuen Speicherblock fragen, direkt zum Ende des Heaps gelangt und Ihnen einen Block gibt. Auf diese Weise erfolgt die Speicherzuweisung genauso schnell wie die Zuweisung auf dem Stapel (wie Sie es die meiste Zeit in C / C ++ tun, aber abgesehen davon ..)
Allokationen sind also so schnell wie nichts anderes als ... das zählt nicht die Kosten für die Freigabe des Speichers. Nur weil Sie erst viel später etwas freigeben, bedeutet dies nicht, dass es nicht viel kostet, und im Fall eines GC-Systems sind die Kosten viel höher als bei "normalen" Heap-Zuweisungen - nicht nur bei den GC muss alle Objekte durchlaufen, um zu sehen, ob sie leben oder nicht, und sie müssen dann auch freigegeben werden, und (der hohe Preis) den Speicher herumkopieren, um den Heap zu komprimieren - damit Sie die schnelle Zuordnung am Ende haben Mechanismus (oder wenn Ihnen der Arbeitsspeicher ausgeht, durchsucht C / C ++ beispielsweise jede Zuordnung nach dem nächsten freien Speicherblock, der für das Objekt geeignet ist).
Dies ist einer der Gründe, warum Java / .NET-Benchmarks eine so gute Leistung aufweisen, während reale Anwendungen eine so schlechte Leistung aufweisen. Ich muss mir nur die Apps auf meinem Handy ansehen - die wirklich schnellen, reaktionsschnellen werden alle mit dem NDK geschrieben, so sehr, dass selbst ich überrascht war.
Sammlungen können heutzutage schnell sein, wenn alle Objekte lokal zugeordnet werden, z. B. in einem einzigen zusammenhängenden Block. In Java erhalten Sie jetzt einfach keine zusammenhängenden Blöcke mehr, da die Objekte nacheinander vom freien Ende des Heapspeichers zugewiesen werden. Sie können glücklich nebeneinander enden, aber nur durch Glück (dh nach Lust und Laune der GC-Komprimierungsroutinen und wie Objekte kopiert werden). C / C ++ unterstützt dagegen explizit zusammenhängende Zuordnungen (über den Stack natürlich). Im Allgemeinen unterscheiden sich Heap-Objekte in C / C ++ nicht von Javas BTW.
Mit C / C ++ können Sie jetzt besser werden als mit den Standardzuordnern, mit denen Speicher gespart und effizient genutzt werden soll. Sie können den Zuweiser durch eine Reihe von Pools mit festen Blöcken ersetzen, sodass Sie immer einen Block finden, der genau die richtige Größe für das zuzuweisende Objekt hat. Das Gehen über den Haufen ist nur eine Frage der Bitmap-Suche, um festzustellen, wo sich ein freier Block befindet, und das Aufheben der Zuordnung setzt einfach ein Bit in dieser Bitmap zurück. Die Kosten sind, dass Sie mehr Speicher verwenden, wenn Sie Blöcke mit fester Größe zuweisen, sodass Sie einen Heap von 4-Byte-Blöcken, einen weiteren für 16-Byte-Blöcke usw. haben.
quelle
Eden Space
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 oderoperator 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 ist
malloc
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
anrufenoperator 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
oderoperator 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.
quelle
Es kommt darauf an, wer die Geschwindigkeit misst, welche Implementierungsgeschwindigkeit sie messen und was sie beweisen wollen. Und was sie vergleichen.
Wenn Sie sich nur das Zuweisen / Aufheben der Zuweisung ansehen, könnten Sie in C ++ 1.000.000 Aufrufe an malloc und 1.000.000 Aufrufe an free () haben. In Java würden 1.000.000 Aufrufe von new () und ein Garbage Collector in einer Schleife ausgeführt, um 1.000.000 Objekte zu finden, die freigegeben werden können. Die Schleife kann schneller sein als der free () -Aufruf.
Andererseits hat malloc / free die andere Zeit verkürzt, und malloc / free setzt normalerweise nur ein Bit in einer separaten Datenstruktur und ist für das Auftreten von malloc / free im selben Thread optimiert, sodass in einer Multithread-Umgebung keine gemeinsam genutzten Speichervariablen vorhanden sind werden in vielen Fällen verwendet (und Sperr- oder gemeinsam genutzte Speichervariablen sind sehr teuer).
Auf der dritten Seite gibt es Dinge wie das Referenzzählen, die Sie möglicherweise ohne Garbage Collection benötigen, und das ist nicht kostenlos.
quelle