ByteBuffer.allocate () vs. ByteBuffer.allocateDirect ()

144

Zu allocate()oder zu allocateDirect(), das ist die Frage.

Seit einigen Jahren bin ich nur bei dem Gedanken geblieben, dass DirectByteBuffers , da es sich um eine direkte Speicherzuordnung auf Betriebssystemebene handelt, bei get / put-Aufrufen schneller funktioniert als HeapByteBuffers. Ich war bis jetzt nie wirklich daran interessiert, die genauen Details der Situation herauszufinden. Ich möchte wissen, welche der beiden Arten von ByteBuffers unter welchen Bedingungen schneller sind.

ROMANIA_engineer
quelle
Um eine bestimmte Antwort zu geben, müssen Sie genau sagen, was Sie mit ihnen machen. Wenn einer immer schneller als der andere wäre, warum gäbe es dann zwei Varianten? Vielleicht können Sie näher erläutern, warum Sie jetzt "wirklich daran interessiert sind, die genauen Details herauszufinden". Übrigens: Haben Sie den Code gelesen, insbesondere für DirectByteBuffer?
Peter Lawrey
Sie werden zum Lesen und Schreiben von SocketChannels verwendet, die für die Nichtblockierung konfiguriert sind. In Bezug auf das, was @bmargulies gesagt hat, wird DirectByteBuffers für die Kanäle schneller abschneiden .
@Gnarly Zumindest die aktuelle Version meiner Antwort besagt, dass Kanäle voraussichtlich davon profitieren werden.
Bmargulies

Antworten:

150

Ron Hitches scheint in seinem ausgezeichneten Buch Java NIO das zu bieten, was ich für eine gute Antwort auf Ihre Frage hielt:

Betriebssysteme führen E / A-Operationen in Speicherbereichen aus. Diese Speicherbereiche sind für das Betriebssystem zusammenhängende Folgen von Bytes. Kein Wunder also, dass nur Bytepuffer zur Teilnahme an E / A-Vorgängen berechtigt sind. Denken Sie auch daran, dass das Betriebssystem direkt auf den Adressraum des Prozesses zugreift, in diesem Fall auf den JVM-Prozess, um die Daten zu übertragen. Dies bedeutet, dass Speicherbereiche, die Ziele von E / A-Perationen sind, zusammenhängende Folgen von Bytes sein müssen. In der JVM wird ein Array von Bytes möglicherweise nicht zusammenhängend im Speicher gespeichert, oder der Garbage Collector kann es jederzeit verschieben. Arrays sind Objekte in Java, und die Art und Weise, wie Daten in diesem Objekt gespeichert werden, kann von einer JVM-Implementierung zur anderen variieren.

Aus diesem Grund wurde der Begriff eines direkten Puffers eingeführt. Direkte Puffer sind für die Interaktion mit Kanälen und nativen E / A-Routinen vorgesehen. Sie bemühen sich nach besten Kräften, die Byteelemente in einem Speicherbereich zu speichern, den ein Kanal für den direkten oder unformatierten Zugriff verwenden kann, indem sie nativen Code verwenden, um das Betriebssystem anzuweisen, den Speicherbereich direkt zu entleeren oder zu füllen.

Direktbytepuffer sind normalerweise die beste Wahl für E / A-Operationen. Sie unterstützen den effizientesten E / A-Mechanismus, der der JVM zur Verfügung steht. Nicht direkte Bytepuffer können an Kanäle übergeben werden, dies kann jedoch zu Leistungseinbußen führen. Es ist normalerweise nicht möglich, dass ein nicht direkter Puffer das Ziel einer nativen E / A-Operation ist. Wenn Sie ein nicht direktes ByteBuffer-Objekt zum Schreiben an einen Kanal übergeben, kann der Kanal bei jedem Aufruf implizit Folgendes ausführen:

  1. Erstellen Sie ein temporäres direktes ByteBuffer-Objekt.
  2. Kopieren Sie den Inhalt des nicht direkten Puffers in den temporären Puffer.
  3. Führen Sie die E / A-Operation auf niedriger Ebene mit dem temporären Puffer aus.
  4. Das temporäre Pufferobjekt verlässt den Gültigkeitsbereich und wird schließlich durch Müll gesammelt.

Dies kann möglicherweise zu Pufferkopien und Objektabwanderung bei jeder E / A führen. Dies sind genau die Dinge, die wir vermeiden möchten. Abhängig von der Implementierung sind die Dinge jedoch möglicherweise nicht so schlecht. Die Laufzeit wird wahrscheinlich direkte Puffer zwischenspeichern und wiederverwenden oder andere clevere Tricks ausführen, um den Durchsatz zu steigern. Wenn Sie lediglich einen Puffer für die einmalige Verwendung erstellen, ist der Unterschied nicht signifikant. Wenn Sie den Puffer jedoch in einem Hochleistungsszenario wiederholt verwenden, ist es besser, direkte Puffer zuzuweisen und wiederzuverwenden.

Direkte Puffer sind für E / A optimal, ihre Erstellung ist jedoch möglicherweise teurer als nicht direkte Bytepuffer. Der von direkten Puffern verwendete Speicher wird durch Aufrufen von nativem, betriebssystemspezifischem Code unter Umgehung des Standard-JVM-Heaps zugewiesen. Das Einrichten und Herunterfahren von direkten Puffern kann je nach Host-Betriebssystem und JVM-Implementierung erheblich teurer sein als Heap-residente Puffer. Die Speicherbereiche von Direktpuffern unterliegen keiner Speicherbereinigung, da sie sich außerhalb des Standard-JVM-Heaps befinden.

Die Leistungskompromisse bei der Verwendung von direkten und nicht direkten Puffern können je nach JVM, Betriebssystem und Code-Design stark variieren. Indem Sie Speicher außerhalb des Heapspeichers zuweisen, können Sie Ihre Anwendung zusätzlichen Kräften aussetzen, von denen die JVM nichts weiß. Stellen Sie sicher, dass Sie den gewünschten Effekt erzielen, wenn Sie zusätzliche bewegliche Teile ins Spiel bringen. Ich empfehle die alte Software-Maxime: Lass es zuerst funktionieren, dann mache es schnell. Sorgen Sie sich nicht zu sehr um die Optimierung im Vorfeld. Konzentrieren Sie sich zuerst auf die Richtigkeit. Die JVM-Implementierung kann möglicherweise Puffer-Caching oder andere Optimierungen durchführen, die Ihnen die Leistung bieten, die Sie benötigen, ohne dass Sie unnötigen Aufwand betreiben müssen.

Edwin Dalorzo
quelle
9
Ich mag dieses Zitat nicht, weil es zu viele Vermutungen enthält. Außerdem muss die JVM beim Ausführen von E / A für einen nicht direkten ByteBuffer sicherlich keinen direkten ByteBuffer zuweisen: Es reicht aus, eine Folge von Bytes auf dem Heap zuzuordnen, die E / A auszuführen, von den Bytes in den ByteBuffer zu kopieren und die Bytes freizugeben. Diese Bereiche könnten sogar zwischengespeichert werden. Es ist jedoch völlig unnötig, dafür ein Java-Objekt zuzuweisen. Echte Antworten werden nur durch Messen erhalten. Als ich das letzte Mal Messungen durchgeführt habe, gab es keinen signifikanten Unterschied. Ich müsste Tests wiederholen, um alle spezifischen Details zu erhalten.
Robert Klemme
4
Es ist fraglich, ob ein Buch, das NIO (und native Operationen) beschreibt, Gewissheiten enthalten kann. Schließlich verwalten verschiedene JVMs und Betriebssysteme die Dinge unterschiedlich, sodass der Autor nicht dafür verantwortlich gemacht werden kann, dass er bestimmte Verhaltensweisen nicht garantieren kann.
Martin Tuskevicius
@RobertKlemme, +1, wir alle hassen das Rätselraten. Es kann jedoch unmöglich sein, die Leistung für alle wichtigen Betriebssysteme zu messen, da es einfach viel zu viele wichtige Betriebssysteme gibt. Ein anderer Beitrag hat dies versucht, aber wir können viele, viele Probleme mit dem Benchmark sehen, beginnend mit "Die Ergebnisse schwanken stark je nach Betriebssystem". Was ist auch, wenn es ein schwarzes Schaf gibt, das schreckliche Dinge wie Pufferkopien auf jeder E / A ausführt? Dann könnten wir aufgrund dieses Schafs gezwungen sein, das Schreiben von Code zu verhindern, den wir sonst verwenden würden, nur um diese Worst-Case-Szenarien zu vermeiden.
Pacerier
@ RobertKlemme Ich stimme zu. Hier wird viel zu viel geraten. Es ist beispielsweise unwahrscheinlich, dass die JVM Byte-Arrays nur spärlich zuweist.
Marquis von Lorne
@ Edwin Dalorzo: Warum brauchen wir einen solchen Byte-Puffer in der realen Welt? Werden sie als Hack erfunden, um das Gedächtnis zwischen den Prozessen zu teilen? Angenommen, JVM wird auf einem Prozess ausgeführt, und es handelt sich um einen anderen Prozess, der auf der Netzwerk- oder Datenverbindungsschicht ausgeführt wird, die für die Übertragung der Daten verantwortlich ist. Werden diese Bytepuffer zugewiesen, um den Speicher zwischen diesen Prozessen zu teilen? Bitte korrigieren Sie mich, wenn ich falsch liege ..
Tom Taylor
25

Es gibt keinen Grund zu der Annahme , dass direkte Puffer für den Zugriff innerhalb des JVM schneller sind. Ihr Vorteil ergibt sich, wenn Sie sie an nativen Code übergeben - beispielsweise an den Code hinter Kanälen aller Art.

bmargulies
quelle
Tatsächlich. Zum Beispiel, wenn Sie E / A in Scala / Java ausführen und eingebettete Python / native Bibliotheken mit großen Speicherdaten für die algorithmische Verarbeitung aufrufen oder Daten direkt an eine GPU in Tensorflow senden müssen.
SemanticBeeng
21

da DirectByteBuffers eine direkte Speicherzuordnung auf Betriebssystemebene sind

Sie sind nicht. Sie sind nur normaler Anwendungsprozessspeicher, können jedoch während Java GC nicht verschoben werden, was die Dinge innerhalb der JNI-Schicht erheblich vereinfacht. Was Sie beschreiben, gilt für MappedByteBuffer.

dass es mit get / put-Anrufen schneller funktioniert

Die Schlussfolgerung folgt nicht aus der Prämisse; die Prämisse ist falsch; und die Schlussfolgerung ist auch falsch. Sie sind schneller, sobald Sie sich in der JNI-Ebene befinden, und wenn Sie von derselben lesen und schreiben DirectByteBuffer, sind sie viel schneller, da die Daten die JNI-Grenze überhaupt nicht überschreiten müssen.

Marquis von Lorne
quelle
7
Dies ist ein guter und wichtiger Punkt: auf dem Weg von IO Sie das Java überqueren - JNI Grenze zu einem gewissen Punkt. Direkte und nicht direkte Bytepuffer verschieben nur die Grenze: Bei einem direkten Puffer müssen alle Put-Operationen aus Java-Land gekreuzt werden, während bei einem nicht direkten Puffer alle E / A-Operationen gekreuzt werden müssen. Was schneller ist, hängt von der Anwendung ab.
Robert Klemme
@ RobertKlemme Ihre Zusammenfassung ist falsch. Bei allen Puffern müssen alle Daten, die von und nach Java kommen, die JNI-Grenze überschreiten. Der Punkt direkter Puffer ist, dass Sie, wenn Sie nur die Daten von einem Kanal auf einen anderen kopieren, z. B. eine Datei hochladen, diese überhaupt nicht in Java importieren müssen, was viel schneller ist.
Marquis von Lorne
Wo genau ist meine Zusammenfassung falsch? Und mit welcher "Zusammenfassung" am Anfang? Ich habe explizit über "Put-Operationen aus Java-Land" gesprochen. Wenn Sie nur Daten zwischen Kanälen kopieren (dh nie mit den Daten in Java-Land umgehen müssen), ist das natürlich eine andere Geschichte.
Robert Klemme
@RobertKlemme Ihre Aussage, dass 'mit einem direkten Puffer [nur] alle Put-Operationen aus Java-Land gekreuzt werden müssen', ist falsch. Beide bekommen und setzen müssen kreuzen.
Marquis von Lorne
EJP, anscheinend fehlt Ihnen immer noch die beabsichtigte Unterscheidung, die @RobertKlemme getroffen hat, indem Sie die Wörter "Put-Operationen" in einer Phrase und die Wörter "IO-Operationen" in der kontrastierenden Phrase des Satzes verwendet haben. In letzterem Satz sollte er sich auf Operationen zwischen dem Puffer und einem vom Betriebssystem bereitgestellten Gerät beziehen.
Naki
18

Am besten machen Sie Ihre eigenen Messungen. Die schnelle Antwort scheint zu sein, dass das Senden aus einem allocateDirect()Puffer allocate()je nach Größe 25% bis 75% weniger Zeit in Anspruch nimmt als die Variante (getestet als Kopieren einer Datei nach / dev / null), die Zuordnung selbst jedoch erheblich langsamer sein kann (selbst durch ein Faktor von 100x).

Quellen:

Raph Levien
quelle
Vielen Dank. Ich würde Ihre Antwort akzeptieren, suche aber nach genaueren Details zu den Leistungsunterschieden.