2011-10-12 16 views
15

Durante l'elaborazione di più file gigabyte ho notato qualcosa di strano: sembra che la lettura da un file utilizzando un file-channel in un oggetto ByteBuffer riutilizzato allocato con allocateDirect sia molto più lenta della lettura da un MappedByteBuffer, infatti è anche più lento della lettura in array di byte usando normali chiamate di lettura!Java Prestazioni prestazioni buffer Byte

mi aspettavo di essere (quasi) più veloce la lettura da mappedbytebuffers come il mio ByteBuffer viene allocata con allocateDirect, da qui la lettura dovrebbe finire-up direttamente nel mio ByteBuffer senza copie intermedie.

La mia domanda ora è: che cosa sto facendo di sbagliato? O il bytebuffer + filechannel è davvero lento rispetto al normale io/mmap?

I il codice di esempio seguente ho aggiunto anche del codice che converte ciò che viene letto in valori lunghi, poiché questo è ciò che il mio codice reale fa costantemente. Mi aspetto che il metodo ByteBuffer getLong() sia molto più veloce del mio byte shuffeler.

Test-risultati: mmap: 3.828 ByteBuffer: 55,097 regolari I/O: 38,175

import java.io.File; 
import java.io.IOException; 
import java.io.RandomAccessFile; 
import java.nio.ByteBuffer; 
import java.nio.channels.FileChannel; 
import java.nio.channels.FileChannel.MapMode; 
import java.nio.MappedByteBuffer; 

class testbb { 
    static final int size = 536870904, n = size/24; 

    static public long byteArrayToLong(byte [] in, int offset) { 
     return ((((((((long)(in[offset + 0] & 0xff) << 8) | (long)(in[offset + 1] & 0xff)) << 8 | (long)(in[offset + 2] & 0xff)) << 8 | (long)(in[offset + 3] & 0xff)) << 8 | (long)(in[offset + 4] & 0xff)) << 8 | (long)(in[offset + 5] & 0xff)) << 8 | (long)(in[offset + 6] & 0xff)) << 8 | (long)(in[offset + 7] & 0xff); 
    } 

    public static void main(String [] args) throws IOException { 
     long start; 
     RandomAccessFile fileHandle; 
     FileChannel fileChannel; 

     // create file 
     fileHandle = new RandomAccessFile("file.dat", "rw"); 
     byte [] buffer = new byte[24]; 
     for(int index=0; index<n; index++) 
      fileHandle.write(buffer); 
     fileChannel = fileHandle.getChannel(); 

     // mmap() 
     MappedByteBuffer mbb = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, size); 
     byte [] buffer1 = new byte[24]; 
     start = System.currentTimeMillis(); 
     for(int index=0; index<n; index++) { 
       mbb.position(index * 24); 
       mbb.get(buffer1, 0, 24); 
       long dummy1 = byteArrayToLong(buffer1, 0); 
       long dummy2 = byteArrayToLong(buffer1, 8); 
       long dummy3 = byteArrayToLong(buffer1, 16); 
     } 
     System.out.println("mmap: " + (System.currentTimeMillis() - start)/1000.0); 

     // bytebuffer 
     ByteBuffer buffer2 = ByteBuffer.allocateDirect(24); 
     start = System.currentTimeMillis(); 
     for(int index=0; index<n; index++) { 
      buffer2.rewind(); 
      fileChannel.read(buffer2, index * 24); 
      buffer2.rewind(); // need to rewind it to be able to use it 
      long dummy1 = buffer2.getLong(); 
      long dummy2 = buffer2.getLong(); 
      long dummy3 = buffer2.getLong(); 
     } 
     System.out.println("bytebuffer: " + (System.currentTimeMillis() - start)/1000.0); 

     // regular i/o 
     byte [] buffer3 = new byte[24]; 
     start = System.currentTimeMillis(); 
     for(int index=0; index<n; index++) { 
       fileHandle.seek(index * 24); 
       fileHandle.read(buffer3); 
       long dummy1 = byteArrayToLong(buffer1, 0); 
       long dummy2 = byteArrayToLong(buffer1, 8); 
       long dummy3 = byteArrayToLong(buffer1, 16); 
     } 
     System.out.println("regular i/o: " + (System.currentTimeMillis() - start)/1000.0); 
    } 
} 

Come caricare grandi sezioni e poi di elaborazione è di loro non è un'opzione (io sto leggendo i dati dappertutto) Penso che dovrei attenermi a un MappedByteBuffer. Grazie a tutti per i vostri suggerimenti.

risposta

9

Credo che stai solo facendo micro-ottimizzazione, which might just not matter (www.codinghorror.com).

Di seguito è una versione con un buffer più grande e chiamate ridondanti seek/ rimosse.

  • Quando abilito "ordinamento nativo byte" (che in realtà non sicuro se la macchina utilizza un 'endian' convenzione diversa):
mmap: 1.358 
bytebuffer: 0.922 
regular i/o: 1.387 
  • Quando io commento la dichiarazione dell'ordine e utilizzare l'ordine big-endian predefinito:
mmap: 1.336 
bytebuffer: 1.62 
regular i/o: 1.467 
  • tuo codice originale:
mmap: 3.262 
bytebuffer: 106.676 
regular i/o: 90.903 

Ecco il codice:

import java.io.File; 
import java.io.IOException; 
import java.io.RandomAccessFile; 
import java.nio.ByteBuffer; 
import java.nio.ByteOrder; 
import java.nio.channels.FileChannel; 
import java.nio.channels.FileChannel.MapMode; 
import java.nio.MappedByteBuffer; 

class Testbb2 { 
    /** Buffer a whole lot of long values at the same time. */ 
    static final int BUFFSIZE = 0x800 * 8; // 8192 
    static final int DATASIZE = 0x8000 * BUFFSIZE; 

    static public long byteArrayToLong(byte [] in, int offset) { 
     return ((((((((long)(in[offset + 0] & 0xff) << 8) | (long)(in[offset + 1] & 0xff)) << 8 | (long)(in[offset + 2] & 0xff)) << 8 | (long)(in[offset + 3] & 0xff)) << 8 | (long)(in[offset + 4] & 0xff)) << 8 | (long)(in[offset + 5] & 0xff)) << 8 | (long)(in[offset + 6] & 0xff)) << 8 | (long)(in[offset + 7] & 0xff); 
    } 

    public static void main(String [] args) throws IOException { 
     long start; 
     RandomAccessFile fileHandle; 
     FileChannel fileChannel; 

     // Sanity check - this way the convert-to-long loops don't need extra bookkeeping like BUFFSIZE/8. 
     if ((DATASIZE % BUFFSIZE) > 0 || (DATASIZE % 8) > 0) { 
      throw new IllegalStateException("DATASIZE should be a multiple of 8 and BUFFSIZE!"); 
     } 

     int pos; 
     int nDone; 

     // create file 
     File testFile = new File("file.dat"); 
     fileHandle = new RandomAccessFile("file.dat", "rw"); 

     if (testFile.exists() && testFile.length() >= DATASIZE) { 
      System.out.println("File exists"); 
     } else { 
      testFile.delete(); 
      System.out.println("Preparing file"); 
      byte [] buffer = new byte[BUFFSIZE]; 
      pos = 0; 
      nDone = 0; 
      while (pos < DATASIZE) { 
       fileHandle.write(buffer); 
       pos += buffer.length; 
      } 

      System.out.println("File prepared"); 
     } 
     fileChannel = fileHandle.getChannel(); 

     // mmap() 
     MappedByteBuffer mbb = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, DATASIZE); 
     byte [] buffer1 = new byte[BUFFSIZE]; 
     mbb.position(0); 
     start = System.currentTimeMillis(); 
     pos = 0; 
     while (pos < DATASIZE) { 
      mbb.get(buffer1, 0, BUFFSIZE); 
      // This assumes BUFFSIZE is a multiple of 8. 
      for (int i = 0; i < BUFFSIZE; i += 8) { 
       long dummy = byteArrayToLong(buffer1, i); 
      } 
      pos += BUFFSIZE; 
     } 
     System.out.println("mmap: " + (System.currentTimeMillis() - start)/1000.0); 

     // bytebuffer 
     ByteBuffer buffer2 = ByteBuffer.allocateDirect(BUFFSIZE); 
//  buffer2.order(ByteOrder.nativeOrder()); 
     buffer2.order(); 
     fileChannel.position(0); 
     start = System.currentTimeMillis(); 
     pos = 0; 
     nDone = 0; 
     while (pos < DATASIZE) { 
      buffer2.rewind(); 
      fileChannel.read(buffer2); 
      buffer2.rewind(); // need to rewind it to be able to use it 
      // This assumes BUFFSIZE is a multiple of 8. 
      for (int i = 0; i < BUFFSIZE; i += 8) { 
       long dummy = buffer2.getLong(); 
      } 
      pos += BUFFSIZE; 
     } 
     System.out.println("bytebuffer: " + (System.currentTimeMillis() - start)/1000.0); 

     // regular i/o 
     fileHandle.seek(0); 
     byte [] buffer3 = new byte[BUFFSIZE]; 
     start = System.currentTimeMillis(); 
     pos = 0; 
     while (pos < DATASIZE && nDone != -1) { 
      nDone = 0; 
      while (nDone != -1 && nDone < BUFFSIZE) { 
       nDone = fileHandle.read(buffer3, nDone, BUFFSIZE - nDone); 
      } 
      // This assumes BUFFSIZE is a multiple of 8. 
      for (int i = 0; i < BUFFSIZE; i += 8) { 
       long dummy = byteArrayToLong(buffer3, i); 
      } 
      pos += nDone; 
     } 
     System.out.println("regular i/o: " + (System.currentTimeMillis() - start)/1000.0); 
    } 
} 
+0

Questo sarebbe davvero più veloce. Non mi aspettavo che fosse molto più veloce, quindi grazie! –

+0

Se non sbaglio, la sezione I/O regolare intende utilizzare buffer3 in entrambi i cicli, piuttosto che leggere i lunghi fuori dal buffer1 invariato. –

2

Quando si dispone di un ciclo che itera più di 10.000 volte, può attivare l'intero metodo da compilare nel codice nativo. Tuttavia, i tuoi loop successivi non sono stati eseguiti e non possono essere ottimizzati allo stesso modo. Per evitare questo problema, posiziona ciascun ciclo in un metodo diverso ed esegui di nuovo.

Inoltre, è possibile impostare l'ordine per ByteBuffer come ordine (ByteOrder.nativeOrder()) per evitare che tutti i byte si scambino quando si esegue un getLong e si leggano più di 24 byte alla volta. (Poiché la lettura di porzioni molto piccole genera molte più chiamate di sistema) Prova a leggere 32 * 1024 byte alla volta.

Ho anche provato getLong sul MappedByteBuffer con l'ordine di byte nativo. Questo è probabilmente il più veloce.

+0

Lo spostamento del codice in metodi separati non ha fatto alcuna differenza. Anche l'utilizzo di getLong nel mappedbytebuffer lo ha reso ancora più veloce. Ma mi chiedo ancora perché il secondo test ("leggi un bytebuffer da un canale") sia così lento. \ –

+1

Stai eseguendo una chiamata di sistema ogni 24 byte. Nel primo esempio, si stanno eseguendo solo una o due chiamate di sistema totali. –

0

A MappedByteBuffer sarà sempre il più veloce, poiché il sistema operativo associa il buffer del disco a livello di sistema operativo con lo spazio di memoria del processo. La lettura in un buffer diretto allocato, per confronto, carica prima il blocco nel buffer del sistema operativo, quindi copia il contenuto del buffer del sistema operativo nel buffer in-process allocato.

Il codice di prova esegue anche letture molto piccole (24 byte). Se la tua applicazione effettiva fa lo stesso, otterrai un incremento delle prestazioni ancora maggiore dalla mappatura del file, poiché ciascuna delle letture è una chiamata kernel separata. Dovresti vedere più volte le prestazioni mappando.

Per quanto riguarda il buffer diretta essendo più lento del java.io legge: non si danno tutti i numeri, ma mi aspetto un leggero degrado perché i getLong() chiamate hanno bisogno di attraversare il confine JNI.

+3

Da quello che ho letto (in un libro su NIO da oilly), una lettura su un bytebuffer correttamente assegnato dovrebbe anche essere diretta senza alcuna copia. Purtroppo il mapping del file di input alla memoria non funzionerà nell'app reale in quanto può avere dimensioni pari a terabyte. I numeri erano nella parte inferiore della mia posta: mmap: 3.828 secondi bytebuffer: 55.097 secondi I/O regolari: 38.175 secondi. –

+0

@Folkert - o l'autore di quel libro era sbagliato, o stai interpretando male quello che ha detto. I controller del disco si occupano di blocchi di grandi dimensioni e il sistema operativo ha bisogno di un posto in cui memorizzare i dati e ritagliare il pezzo che ti serve. – kdgregory

+1

Ma il vero problema è che ciascuna delle tue letture - in NIO o IO - è una chiamata di sistema separata, mentre il file mappato è un accesso diretto alla memoria (con un possibile errore di pagina). Se la tua applicazione reale ha una grande proporzione di letture localizzate, probabilmente trarrai beneficio da una cache buffer (che può essere mappata in memoria o on-heap). Se stai saltando su un file in scala terabyte, allora l'I/O del disco diventerà il fattore limitante e persino la mappatura della memoria non sarà di aiuto. – kdgregory

5

lettura nel byte diretta buffer è più veloce, ma ricevendo il dati da esso in th e JVM è più lento. Il buffer di byte diretto è destinato ai casi in cui copi semplicemente i dati senza effettivamente guardarli nel codice Java. Quindi non deve assolutamente attraversare il confine nativo-> JVM, quindi è più veloce dell'uso di es. un byte [] array o un ByteBuffer normale, in cui i dati dovrebbero attraversare il confine due volte nel processo di copia.