2011-10-12 29 views
15

Podczas przetwarzania wielu gigabajtowych plików zauważyłem coś dziwnego: wygląda na to, że odczytywanie pliku za pomocą filechannelu do ponownie używanego obiektu ByteBuffer przydzielonego za pomocą allocateDirect jest znacznie wolniejsze niż czytanie z MappedByteBuffer, w rzeczywistości jest to nawet wolniej niż czytanie w tablicach bajtowych za pomocą zwykłych odczytów!Problem z wydajnością Java ByteBuffer

Spodziewałem się, że będzie on (prawie) tak szybki, jak odczyt z mappedbytebuffers, ponieważ mój ByteBuffer jest alokowany z allocateDirect, stąd odczyt powinien kończyć się bezpośrednio w moim buforze bajtowym bez żadnych kopii pośrednich.

Moje pytanie brzmi: co takiego robię źle? Czy też jest bajtbuffer + filechannel naprawdę mniejszy niż zwykły io/mmap?

I przykładowy kod poniżej Dodałem także kod, który konwertuje to, co czyta się na długie wartości, ponieważ tak właśnie robi to mój prawdziwy kod. Spodziewam się, że metoda ByteBuffer getLong() jest znacznie szybsza niż mój własny shuffeler bajtów.

Test-Wyniki: mmap: 3,828 ByteBuffer: 55,097 regularne 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); 
    } 
} 

jak załadunek dużych sekcje, a następnie przetwarzanie jest im nie jest rozwiązaniem (będę czytaj dane w całym miejscu) Myślę, że powinienem trzymać się MappedByteBuffer. Dziękuję wszystkim za sugestie.

Odpowiedz

9

Wierzę, że właśnie robisz mikrooptymalizację, which might just not matter (www.codinghorror.com).

Poniżej znajduje się wersja z większym buforem i zbędnymi połączeniami seek/.

  • Kiedy włączę "native bajt zamawiania" (co jest rzeczywiście niebezpieczne, jeśli urządzenie używa innego 'endian' Konwencja):
mmap: 1.358 
bytebuffer: 0.922 
regular i/o: 1.387 
  • Kiedy wykomentuj Polecenie zamówienia i użyj domyślnego zamówienia big-endian:
mmap: 1.336 
bytebuffer: 1.62 
regular i/o: 1.467 
  • Twój oryginalny kod:
mmap: 3.262 
bytebuffer: 106.676 
regular i/o: 90.903 

Oto kod:

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

To rzeczywiście byłoby szybciej. Nie spodziewałem się, że będzie o wiele szybciej, dzięki! –

+0

Jeśli się nie mylę, regularna sekcja i/o zamierza używać bufora3 w obu pętlach, zamiast czytać długie fragmenty niezmiennego bufora1. –

2

Gdy masz pętlę, która wykonuje iterację ponad 10 000 razy, może uruchomić kompilację całej metody na kod natywny. Jednak twoje późniejsze pętle nie zostały uruchomione i nie można ich zoptymalizować w tym samym stopniu. Aby uniknąć tego problemu, umieść każdą pętlę w innej metodzie i uruchom ponownie.

Dodatkowo, możesz chcieć ustawić kolejność dla ByteBuffer, który ma być zamówiony (ByteOrder.nativeOrder()), aby uniknąć zamiany wszystkich bajtów podczas wykonywania getLong i przeczytać więcej niż 24 bajty na raz. (W przypadku czytania bardzo małych fragmentów generuje znacznie więcej wywołań systemowych) Spróbuj czytać 32 * 1024 bajtów naraz.

I rany również spróbuj getLong na MappedByteBuffer z natywną kolejnością bajtów. To najprawdopodobniej najszybciej.

+0

Przeniesienie kodu do oddzielnych metod nie miało żadnego znaczenia. Używanie także getLong w mappedbytebuffer rzeczywiście uczyniło to jeszcze szybszym. Ale nadal zastanawiam się, dlaczego drugi test ("odczytaj bajtbuffer z zestawu znaków") jest tak powolny. \ –

+1

Wykonujesz jedno wywołanie systemowe na każde 24 bajty. W pierwszym przykładzie wykonujesz tylko jedno lub dwa połączenia systemowe. –

0

Zawsze będzie najszybciej, ponieważ system operacyjny kojarzy bufor dysku na poziomie systemu operacyjnego z przestrzenią pamięci procesowej. Wczytanie do przydzielonego bufora bezpośredniego, w porównaniu, najpierw ładuje blok do bufora systemu operacyjnego, a następnie kopiuje zawartość bufora systemu operacyjnego do przydzielonego bufora wewnętrznego.

Twój kod testowy również wykonuje wiele bardzo małych (24-bajtowych) odczytów. Jeśli twoja rzeczywista aplikacja robi to samo, wtedy otrzymasz jeszcze większą poprawę wydajności dzięki mapowaniu pliku, ponieważ każdy z odczytów jest oddzielnym wywołaniem jądra. Powinieneś zobaczyć kilka razy wydajność poprzez mapowanie.

Jeśli chodzi o bufor bezpośredni, który jest wolniejszy niż plik java.io, brzmi: nie podajesz żadnych liczb, ale spodziewałbym się lekkiej degeneracji, ponieważ wywołania getLong() muszą przekraczać granicę JNI.

+3

Z tego co przeczytałem (w książce o NIO z o'reilly), odczyt do odpowiednio przydzielonego bytebuffera powinien być bezpośredni bez żadnych kopii. Niestety mapowanie pliku wejściowego do pamięci nie działa w rzeczywistej aplikacji, ponieważ może to być terabajt wielkości. Numery znajdowały się na dole mojej poczty: mmap: 3,828 sekund bajtów: 55.097 sekund regularnych we/wy: 38,175 sekund. –

+0

@Folkert - albo autor tej książki był w błędzie, albo źle interpretujesz to, co powiedział. Kontrolery dysków zajmują się dużymi rozmiarami bloków, a system operacyjny potrzebuje miejsca do buforowania tych danych i wydzielania potrzebnego elementu. – kdgregory

+1

Ale prawdziwy problem polega na tym, że każdy twój odczyt - w NIO lub IO - jest oddzielnym wywołaniem systemowym, podczas gdy zmapowany plik jest bezpośrednim dostępem do pamięci (z możliwą awarią strony). Jeśli twoja prawdziwa aplikacja ma dużą część zlokalizowanych odczytów, prawdopodobnie skorzystasz z bufora podręcznego bufora (który może być mapowany w pamięci lub na stosie). Jeśli przeskakujesz cały plik o rozmiarze terabajtowym, to dysk IO stanie się czynnikiem ograniczającym, a nawet mapowanie pamięci nie pomoże. – kdgregory

5

Reading do bezpośredniego bajt bufora jest szybsza, ale uzyskiwanie dane z tego do th JVM działa wolniej. Bezpośredni bufor bajtowy jest przeznaczony dla przypadków, w których po prostu kopiujesz dane bez faktycznego oglądania ich w kodzie Java. Wtedy wcale nie musi przekraczać granicy natywnej-> JVM, więc jest to szybsze niż użycie np. tablica bajtów [] lub normalny ByteBuffer, gdzie dane musiałyby dwukrotnie przekroczyć tę granicę w procesie kopiowania.

Powiązane problemy