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


144

A allocate()o a allocateDirect(), questa è la domanda.

Ormai da alcuni anni mi sono appena bloccato al pensiero che da allora DirectByteBuffer s è una mappatura diretta della memoria a livello di sistema operativo, avrebbe prestazioni più veloci con le chiamate get / put rispetto a HeapByteBuffers. Non sono mai stato veramente interessato a scoprire i dettagli esatti sulla situazione fino ad ora. Voglio sapere quali dei due tipi di ByteBuffers sono più veloci e su quali condizioni.


Per dare una risposta specifica, devi dire in modo specifico cosa stai facendo con loro. Se uno era sempre più veloce dell'altro, perché ci sarebbero due varianti. Forse puoi ampliare il motivo per cui ora sei "veramente interessato a scoprire i dettagli esatti" A proposito: hai letto il codice, specialmente per DirectByteBuffer?
Peter Lawrey,

Saranno utilizzati per leggere e scrivere su messaggi SocketChannelconfigurati per il non blocco. Quindi, per quanto riguarda ciò che ha detto @bmargulies, DirectByteBuffers funzionerà più velocemente per i canali.

@Gnarly Almeno l'attuale versione della mia risposta dice che i canali dovrebbero trarne beneficio.
bmargulies,

Risposte:


150

Ron Hitches nel suo eccellente libro Java NIO sembra offrire quella che pensavo potesse essere una buona risposta alla tua domanda:

I sistemi operativi eseguono operazioni di I / O su aree di memoria. Queste aree di memoria, per quanto riguarda il sistema operativo, sono sequenze contigue di byte. Non sorprende quindi che solo i buffer di byte siano idonei a partecipare alle operazioni di I / O. Ricorda inoltre che il sistema operativo accederà direttamente allo spazio degli indirizzi del processo, in questo caso il processo JVM, per trasferire i dati. Ciò significa che le aree di memoria che sono target delle perazioni I / O devono essere sequenze contigue di byte. Nella JVM, un array di byte potrebbe non essere archiviato in modo contiguo nella memoria, oppure Garbage Collector potrebbe spostarlo in qualsiasi momento. Le matrici sono oggetti in Java e il modo in cui i dati sono archiviati all'interno di tale oggetto può variare da un'implementazione JVM a un'altra.

Per questo motivo, è stata introdotta la nozione di buffer diretto. I buffer diretti sono destinati all'interazione con i canali e le routine di I / O native. Fanno il possibile per memorizzare gli elementi byte in un'area di memoria che un canale può utilizzare per l'accesso diretto o non elaborato utilizzando il codice nativo per indicare al sistema operativo di svuotare o riempire direttamente l'area di memoria.

I buffer di byte diretti sono in genere la scelta migliore per le operazioni di I / O. In base alla progettazione, supportano il meccanismo I / O più efficiente disponibile per la JVM. I buffer di byte non diretti possono essere passati ai canali, ma ciò potrebbe comportare una penalità di prestazione. Di solito non è possibile che un buffer non diretto sia la destinazione di un'operazione di I / O nativa. Se si passa un oggetto ByteBuffer non diretto a un canale per la scrittura, il canale può implicitamente eseguire le seguenti operazioni su ogni chiamata:

  1. Creare un oggetto ByteBuffer temporaneo diretto.
  2. Copia il contenuto del buffer non indiretto nel buffer temporaneo.
  3. Eseguire l'operazione di I / O di basso livello utilizzando il buffer temporaneo.
  4. L'oggetto buffer temporaneo non rientra nell'ambito di applicazione e viene infine garbage collection.

Ciò può potenzialmente comportare la copia del buffer e lo sfasamento degli oggetti su ogni I / O, che sono esattamente il genere di cose che vorremmo evitare. Tuttavia, a seconda dell'implementazione, le cose potrebbero non andare così male. Il runtime probabilmente memorizzerà nella cache e riutilizzerà i buffer diretti o eseguirà altri trucchi intelligenti per aumentare la velocità effettiva. Se stai semplicemente creando un buffer per l'uso singolo, la differenza non è significativa. D'altra parte, se si utilizzerà ripetutamente il buffer in uno scenario ad alte prestazioni, è meglio allocare i buffer diretti e riutilizzarli.

I buffer diretti sono ottimali per l'I / O, ma possono essere più costosi da creare rispetto ai buffer di byte non diretti. La memoria utilizzata dai buffer diretti viene allocata chiamando il codice nativo specifico del sistema operativo, ignorando l'heap JVM standard. L'impostazione e lo smantellamento dei buffer diretti potrebbe essere significativamente più costoso rispetto ai buffer residenti in heap, a seconda del sistema operativo host e dell'implementazione di JVM. Le aree di memoria dei buffer diretti non sono soggette alla garbage collection perché si trovano al di fuori dell'heap JVM standard.

I compromessi prestazionali dell'utilizzo di buffer diretti rispetto a quelli non diretti possono variare ampiamente in base a JVM, sistema operativo e progettazione del codice. Allocando la memoria all'esterno dell'heap, è possibile sottoporre l'applicazione a forze aggiuntive di cui la JVM non è a conoscenza. Quando si mettono in gioco ulteriori parti in movimento, assicurarsi di ottenere l'effetto desiderato. Consiglio la vecchia massima del software: prima fatelo funzionare, poi fatelo veloce. Non preoccuparti troppo dell'ottimizzazione iniziale; concentrarsi prima sulla correttezza. L'implementazione di JVM potrebbe essere in grado di eseguire la memorizzazione nella cache del buffer o altre ottimizzazioni che ti offriranno le prestazioni di cui hai bisogno senza troppi sforzi inutili da parte tua.


9
Non mi piace quella citazione perché contiene troppe ipotesi. Inoltre, la JVM non ha certamente bisogno di allocare un ByteBuffer diretto quando si esegue IO per un ByteBuffer non diretto: è sufficiente eseguire il malloc di una sequenza di byte sull'heap, eseguire l'IO, copiare dai byte sul ByteBuffer e rilasciare i byte. Quelle aree potrebbero anche essere memorizzate nella cache. Ma non è assolutamente necessario allocare un oggetto Java per questo. Le risposte reali saranno ottenute solo dalla misurazione. L'ultima volta che ho fatto le misurazioni non c'erano differenze significative. Dovrei ripetere i test per trovare tutti i dettagli specifici.
Robert Klemme,

4
È discutibile se un libro che descrive NIO (e operazioni native) può avere certezze in esso. Dopotutto, JVM e sistemi operativi diversi gestiscono le cose in modo diverso, quindi l'autore non può essere incolpato di non essere in grado di garantire determinati comportamenti.
Martin Tuskevicius,

@RobertKlemme, +1, tutti odiamo le congetture, tuttavia, potrebbe essere impossibile misurare le prestazioni per tutti i principali sistemi operativi, dal momento che ci sono troppi sistemi operativi principali. Un altro post ci ha provato, ma possiamo vedere molti problemi con il suo benchmark, a partire da "i risultati oscillano ampiamente a seconda del sistema operativo". Inoltre, cosa succede se c'è una pecora nera che fa cose orribili come la copia buffer su ogni I / O? Quindi a causa di quella pecora, potremmo essere costretti a impedire la scrittura di codice che altrimenti useremmo, solo per evitare questi scenari peggiori.
Pacerier,

@RobertKlemme Sono d'accordo. Ci sono troppe congetture qui. Ad esempio, è improbabile che la JVM alloca scarsamente gli array di byte.
Marchese di Lorne,

@Edwin Dalorzo: Perché abbiamo bisogno di questo buffer di byte nel mondo reale? Sono stati inventati come hack per condividere la memoria tra i processi? Supponiamo ad esempio che JVM venga eseguito su un processo e sarebbe un altro processo che gira su un livello di rete o di collegamento dati - che è responsabile della trasmissione dei dati - questi buffer di byte sono allocati per condividere la memoria tra questi processi? Per favore, correggimi se sbaglio ..
Tom Taylor,

25

Non c'è motivo di aspettarsi che i buffer diretti siano più veloci per l'accesso all'interno di jvm. Il loro vantaggio deriva dal passaggio a codice nativo, ad esempio il codice dietro canali di ogni tipo.


Infatti. Ad esempio, quando è necessario eseguire IO in Scala / Java e chiamare librerie Python / native incorporate con dati di memoria di grandi dimensioni per l'elaborazione algoritmica o alimentare i dati direttamente a una GPU in Tensorflow.
SemanticBeeng

21

poiché DirectByteBuffers è una mappatura diretta della memoria a livello di sistema operativo

Non lo sono. Sono solo la normale memoria del processo applicativo, ma non sono soggetti a riposizionamento durante il GC Java che semplifica notevolmente le cose all'interno del layer JNI. Ciò che descrivi si applica a MappedByteBuffer.

che avrebbe funzionato più velocemente con le chiamate get / put

La conclusione non segue dalla premessa; la premessa è falsa; e anche la conclusione è falsa. Sono più veloci una volta entrati nel livello JNI e se stai leggendo e scrivendo dallo stesso DirectByteBuffersono molto più veloci, perché i dati non devono mai oltrepassare il confine JNI.


7
Questo è un punto buono e importante: sul percorso di IO devi attraversare il confine Java - JNI ad un certo punto. I buffer di byte diretti e non diretti spostano solo il bordo: con un buffer diretto tutte le operazioni put di Java Land devono attraversare, mentre con un buffer non diretto tutte le operazioni IO devono attraversare. Ciò che è più veloce dipende dall'applicazione.
Robert Klemme,

@RobertKlemme Il tuo riepilogo non è corretto. Con tutti i buffer, qualsiasi dato proveniente da e verso Java deve oltrepassare il confine JNI. Il punto dei buffer diretti è che se si stanno semplicemente copiando i dati da un canale a un altro, ad esempio caricando un file, non è necessario trasferirli in Java, il che è molto più veloce.
Marchese di Lorne,

dove è esattamente il mio sommario errato? E quale "sommario" per cominciare? Stavo esplicitamente parlando di "operazioni put dalla terra di Java". Se copi solo dati tra i canali (cioè non devi mai gestire i dati nella terra Java), questa è ovviamente una storia diversa.
Robert Klemme,

@RobertKlemme L'affermazione secondo cui "con un buffer diretto [solo] tutte le operazioni di put da Java land devono attraversare" non è corretta. Entrambi i punti devono essere incrociati.
Marchese di Lorne,

EJP, apparentemente ti manca ancora la distinzione voluta che @RobertKlemme stava facendo scegliendo di usare le parole "operazioni put" in una frase e usando le parole "operazioni IO" nella frase contrastata della frase. In quest'ultima frase, la sua intenzione era di fare riferimento a operazioni tra il buffer e un dispositivo fornito dal sistema operativo di qualche tipo.
naki,

18

Meglio fare le tue misurazioni. La risposta rapida sembra essere che l'invio da un allocateDirect()buffer richiede dal 25% al ​​75% in meno del tempo rispetto alla allocate()variante (testato come copia di un file in / dev / null), a seconda delle dimensioni, ma che l'allocazione stessa può essere significativamente più lenta (anche da un fattore di 100x).

fonti:


Grazie. Accetterei la tua risposta ma sto cercando alcuni dettagli più specifici riguardo alle differenze di prestazione.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.