2011-10-12 22 views
15

Mientras procesaba varios archivos de gigabytes noté algo extraño: parece que leer un archivo utilizando un canal de archivos en un objeto ByteBuffer reutilizado asignado con allocateDirect es mucho más lento que leer desde un MappedByteBuffer, de hecho es ¡incluso más lento que leer en matrices de bytes usando llamadas de lectura regulares!Problema de rendimiento de Java ByteBuffer

Esperaba que fuera (casi) tan rápido como la lectura de mappedbytebuffers ya que mi ByteBuffer está asignado con allocateDirect, por lo tanto, la lectura debe terminar directamente en mi bytebuffer sin ninguna copia intermedia.

Mi pregunta ahora es: ¿qué es lo que estoy haciendo mal? ¿O es bytebuffer + filechannel realmente más lento que el io/mmap regular?

I el código de ejemplo a continuación También agregué un código que convierte lo que se lee en valores largos, ya que es lo que mi código real hace constantemente. Esperaría que el método byteBuffer getLong() sea mucho más rápido que mi propio shuffeler de bytes.

Test-resultados: mmap: 3.828 ByteBuffer: 55.097 regulares de E/S: 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); 
    } 
} 

Como cargar grandes secciones y luego procesarlos es no es una opción (voy estar leyendo datos en todo el lugar) Creo que debería mantener un MappedByteBuffer. Gracias a todos por sus sugerencias.

Respuesta

9

Creo que solo está haciendo una micro-optimización, which might just not matter (www.codinghorror.com).

A continuación se muestra una versión con un búfer mayor y redundantes seek/ llamadas eliminado.

  • cuando activo "ordenamiento nativa byte" (que en realidad es inseguro si la máquina utiliza una convención 'endian' es diferente):
mmap: 1.358 
bytebuffer: 0.922 
regular i/o: 1.387 
  • Cuando comento hacia fuera la instrucción de la orden y use la ordenación big-endian predeterminada:
mmap: 1.336 
bytebuffer: 1.62 
regular i/o: 1.467 
  • Su código original:
mmap: 3.262 
bytebuffer: 106.676 
regular i/o: 90.903 

Aquí está el código:

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

Eso sería más rápido. No esperaba que fuera mucho más rápido, ¡gracias! –

+0

Si no me equivoco, la sección de E/S regular tiene la intención de utilizar buffer3 en ambos bucles, en lugar de leer longs del buffer1 sin cambios. –

2

Cuando tiene un bucle que itera más de 10.000 veces puede activar todo el método para compilarse en código nativo. Sin embargo, sus bucles posteriores no se han ejecutado y no se pueden optimizar en el mismo grado. Para evitar este problema, coloque cada ciclo en un método diferente y vuelva a ejecutar.

Además, es posible que desee establecer el orden para que ByteBuffer sea orden (ByteOrder.nativeOrder()) para evitar todos los bytes al hacer un getLong y leer más de 24 bytes a la vez. (Como leer porciones muy pequeñas genera muchas más llamadas al sistema) Intente leer 32 * 1024 bytes a la vez.

Herí también intente getLong en el MappedByteBuffer con orden de bytes nativos. Es probable que sea el más rápido.

+0

Mover el código a métodos separados no hizo ninguna diferencia. Usar también getLong en mappedbytebuffer lo hizo aún más rápido. Pero todavía me pregunto por qué la segunda prueba ("leer un bytebuffer de un canal de archivos") es tan lenta. \ –

+1

Está realizando una llamada al sistema por cada 24 bytes. En el primer ejemplo, está realizando solo una o dos llamadas al sistema en total. –

0

A MappedByteBuffer siempre será el más rápido, porque el sistema operativo asocia el búfer de disco del nivel del sistema operativo con su espacio de memoria de proceso. La lectura en un búfer directo asignado, en comparación, primero carga el bloque en el búfer del sistema operativo, luego copia el contenido del búfer del sistema operativo en el búfer en proceso asignado.

Su código de prueba también realiza muchas lecturas muy pequeñas (24 bytes). Si su aplicación real hace lo mismo, obtendrá un impulso de rendimiento aún mayor al mapear el archivo, porque cada una de las lecturas es una llamada separada del kernel. Debería ver varias veces el rendimiento mediante el mapeo.

En cuanto al búfer directo que es más lento que el java.io lee: no se dan números, pero esperaría una ligera degradación porque las llamadas getLong() deben cruzar el límite JNI.

+3

Por lo que leí (en un libro sobre NIO de O'Reilly), una lectura a un bytebuffer correctamente asignado también debería ser directa sin ninguna copia. Desafortunadamente, mapear el archivo de entrada a la memoria no funcionará en la aplicación real, ya que puede tener un tamaño de varios terabytes. Los números estaban en la parte inferior de mi correo: mmap: 3.828 segundos bytebuffer: 55.097 segundos I/O regular: 38.175 segundos. –

+0

@Folkert: o el autor de ese libro estaba equivocado o está malinterpretando lo que dijo. Los controladores de disco se ocupan de los tamaños de bloque grandes, y el sistema operativo necesita un lugar para almacenar esos datos y crear la pieza que necesita. – kdgregory

+1

Pero el problema real es que cada una de sus lecturas, ya sea en NIO o IO, es una llamada de sistema separada, mientras que el archivo asignado es un acceso directo a la memoria (con un posible error de página). Si su aplicación real tiene una gran proporción de lecturas localizadas, es probable que se beneficie de una memoria caché del búfer (que puede mapearse en memoria o acumularse). Si está saltando sobre un archivo a escala de terabyte, entonces el disco IO se convertirá en el factor limitante e incluso la asignación de memoria no ayudará. – kdgregory

5

lectura en el byte directa búfer es más rápido, pero conseguir la datos fuera de ella en th e JVM es más lento. El búfer de byte directo está diseñado para casos en los que simplemente está copiando datos sin mirarlos realmente en el código de Java. Entonces no tiene que cruzar el límite de JVM nativo>, así que es más rápido que usar, por ejemplo, una matriz de byte [] o un ByteBuffer normal, donde los datos tendrían que cruzar ese límite dos veces en el proceso de copia.