2011-10-12 96 views
15

在處理多個千兆字節文件時,我注意到一些奇怪的現象:似乎從使用filechannel的文件讀取分配給allocateDirect的重用ByteBuffer對象比從MappedByteBuffer讀取要慢得多,實際上它是甚至比使用常規讀取調用讀入字節數組更慢!Java ByteBuffer性能問題

由於我的ByteBuffer是用allocateDirect分配的,因此讀取應該直接在我的bytebuffer中直接結束,沒有任何中間拷貝,所以我期待它幾乎像讀取映射緩衝區一樣快。

我現在的問題是:我做錯了什麼?或者是bytebuffer + filechannel真的比普通的io/mmap慢嗎?

我下面的示例代碼我還添加了一些代碼,將讀取的內容轉換爲長整型值,因爲這是我真正的代碼不斷做的。我期望ByteBuffer getLong()方法比我自己的字節分離器快得多。

試驗結果: MMAP:3.828 的ByteBuffer:55.097 常規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); 
    } 
} 

由於裝載大的部分,然後處理它們不是一個選項(我會在各地閱讀數據)我認爲我應該堅持一個MappedByteBuffer。 謝謝大家的建議。

回答

9

我相信你只是在做微優化,which might just not matter (www.codinghorror.com)

以下是帶有較大緩衝區和多餘的seek/呼叫的版本。

  • 當我啓用了 「本地字節序」(這實際上是不安全的,如果該機採用了不同的 '端' 慣例):
mmap: 1.358 
bytebuffer: 0.922 
regular i/o: 1.387 
  • 當我註釋掉訂單聲明並使用默認的大端排序:
mmap: 1.336 
bytebuffer: 1.62 
regular i/o: 1.467 
  • 您的原始代碼:
mmap: 3.262 
bytebuffer: 106.676 
regular i/o: 90.903 

下面的代碼:

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

這確實會更快。沒有想到它會更快,所以謝謝! –

+0

如果我沒有弄錯,常規I/O部分打算在兩個循環中使用buffer3,而不是從不變緩衝區中讀取多個long1。 –

2

當你有一個迭代次數超過10,000次的循環時,它可以觸發整個方法被編譯爲本地代碼。但是,您的後期循環尚未運行,無法對其進行優化。爲避免此問題,請將每個循環放入不同的方法並再次運行。

此外,您可能希望將ByteBuffer的順序設置爲順序(ByteOrder.nativeOrder()),以避免在執行getLong時一次讀取多於24個字節的所有字節。 (因爲讀取非常小的部分會產生更多的系統調用)嘗試一次讀取32 * 1024個字節。

我傷口也嘗試getLong在本地字節順序MappedByteBuffer上。這可能是最快的。

+0

將代碼移入單獨的方法沒有任何區別。 在mappedbytebuffer中使用getLong也確實讓它更快。 但是我仍然想知道爲什麼第二次測試(「從文件通道讀取一個字節緩衝區」)太慢了。\ –

+1

您正在爲每24個字節執行一次系統調用。在第一個示例中,您僅執行一個或兩個系統調用總數。 –

0

A MappedByteBuffer將永遠是最快的,因爲操作系統會將OS級別的磁盤緩衝區與進程內存空間相關聯。通過比較讀入已分配的直接緩衝區,首先將該塊加載到OS緩衝區中,然後將OS緩衝區的內容複製到分配的進程內緩衝區中。

您的測試代碼還有很多非常小的(24字節)讀取。如果您的實際應用程序執行相同的操作,那麼您將通過映射文件獲得更大的性能提升,因爲每次讀取都是獨立的內核調用。你應該看幾次映射的性能。

至於直接緩衝區比java.io的讀取速度慢:您不會給出任何數字,但我期望輕微的降級,因爲getLong()調用需要跨越JNI邊界。

+3

從我讀過的內容(在o'reilly的一本關於NIO的書中),對正確分配的字節緩衝區的讀取也應該是直接的,不需要任何副本。 不幸的是,將輸入文件映射到內存將無法在真正的應用程序中工作,因爲它的大小可能是千兆字節。 這些數字在我的郵件底部:mmap:3.828秒bytebuffer:55.097秒常規I/O:38.175秒。 –

+0

@Folkert - 或者那本書的作者是錯的,或者你誤解了他/她所說的。磁盤控制器處理大塊大小,操作系統需要一個地方來緩衝這些數據並分割出你需要的部分。 – kdgregory

+1

但是真正的問題是,您的每個讀取操作(無論是NIO還是IO)都是單獨的系統調用,而映射文件是直接內存訪問(可能存在頁面錯誤)。如果您的真實應用程序有大量的本地化讀取,您可能會從緩衝區緩存(可以是內存映射或堆內緩存)中受益。如果你跳過一個TB級文件,那麼磁盤IO將成爲限制因素,甚至內存映射也無濟於事。 – kdgregory

5

讀入直接字節緩衝區,也比較快,但得到的數據輸出到th中e JVM速度較慢。直接字節緩衝區適用於只是在沒有在Java代碼中實際查看數據的情況下複製數據的情況。然後,它不必跨越本機 - > JVM邊界,因此比使用例如一個byte []數組或一個正常的ByteBuffer,其中數據將不得不在複製過程中跨越邊界兩次。