【问题标题】:Java ByteBuffer performance issueJava ByteBuffer 性能问题
【发布时间】:2011-12-06 00:39:16
【问题描述】:

在处理多个千兆字节文件时,我注意到一些奇怪的事情:似乎使用文件通道从文件读取到使用 allocateDirect 分配的重用 ByteBuffer 对象比从 MappedByteBuffer 读取要慢得多,实际上它甚至比读取还要慢使用常规读取调用进入字节数组!

我希望它(几乎)与从 mappedbytebuffers 读取一样快,因为我的 ByteBuffer 是使用 allocateDirect 分配的,因此读取应该直接在我的 bytebuffer 中结束,而无需任何中间副本。

我现在的问题是:我做错了什么?或者 bytebuffer+filechannel 真的比普通的 io/mmap 慢吗?

在下面的示例代码中,我还添加了一些将读取的内容转换为长值的代码,因为这就是我的真实代码经常做的事情。我希望 ByteBuffer getLong() 方法比我自己的字节洗牌器快得多。

测试结果: 地图:3.828 字节缓冲区: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。 谢谢大家的建议。

【问题讨论】:

    标签: java performance nio bytebuffer


    【解决方案1】:

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

    以下是具有更大缓冲区并删除了多余的seek / setPosition 调用的版本。

    • 当我启用“本机字节排序”时(如果机器使用不同的“字节序”约定,这实际上是不安全的):
    mmap: 1.358
    bytebuffer: 0.922
    regular i/o: 1.387
    
    • 当我注释掉 order 语句并使用默认的 big-endian 排序时:
    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);
        }
    }
    

    【讨论】:

    • 确实会更快。没想到会这么快,谢谢!
    • 如果我没记错的话,常规 i/o 部分打算在两个循环中使用 buffer3,而不是从不变的 buffer1 中读取 long。
    【解决方案2】:

    读入直接字节缓冲区更快,但将数据从中取出到 JVM 中则更慢。直接字节缓冲区适用于您只是复制数据而没有在 Java 代码中实际查看数据的情况。然后它根本不需要跨越 native->JVM 边界,所以它比使用例如更快。 byte[] 数组或普通 ByteBuffer,其中数据在复制过程中必须两次跨越该边界。

    【讨论】:

      【解决方案3】:

      当您有一个循环超过 10,000 次时,它可以触发将整个方法编译为本机代码。但是,您以后的循环尚未运行,并且无法优化到相同的程度。为避免此问题,请将每个循环置于不同的方法中并再次运行。

      此外,您可能希望将 ByteBuffer 的 Order 设置为 order(ByteOrder.nativeOrder()) 以避免在执行 getLong 并一次读取超过 24 个字节时交换所有字节。 (因为读取非常小的部分会产生更多的系统调用)尝试一次读取 32*1024 字节。

      我还尝试使用本机字节顺序在 MappedByteBuffer 上使用 getLong。这可能是最快的。

      【讨论】:

      • 将代码移动到单独的方法中没有任何区别。在 mappedbytebuffer 中也使用 getLong 确实使它更快。但我仍然想知道为什么第二个测试(“从文件通道读取字节缓冲区”)这么慢。\
      • 您每 24 个字节执行一次系统调用。在第一个示例中,您总共只执行了一个或两个系统调用。
      【解决方案4】:

      MappedByteBuffer 总是最快的,因为操作系统将操作系统级别的磁盘缓冲区与您的进程内存空间相关联。相比之下,读取分配的直接缓冲区首先将块加载到 OS 缓冲区,然后将 OS 缓冲区的内容复制到分配的进程内缓冲区中。

      您的测试代码还会进行大量非常小的(24 字节)读取。如果您的实际应用程序也这样做,那么您将通过映射文件获得更大的性能提升,因为每次读取都是单独的内核调用。通过映射,您应该会看到几倍的性能。

      至于直接缓冲区比 java.io 读取的速度慢:您没有给出任何数字,但我预计会有轻微的降级,因为 getLong() 调用需要跨越 JNI 边界。

      【讨论】:

      • 从我读到的内容(在 o'reilly 的一本关于 NIO 的书中),对正确分配的字节缓冲区的读取也应该是直接的,没有任何副本。不幸的是,将输入文件映射到内存在真正的应用程序中不起作用,因为这可能是 TB 大小。这些数字在我的邮件底部:mmap:3.828 秒字节缓冲区:55.097 秒常规 i/o:38.175 秒。
      • @Folkert - 那本书的作者错了,或者你误解了他/她所说的。磁盘控制器处理大块大小,操作系统需要一个地方来缓冲该数据并划分出您需要的部分。
      • 但真正的问题是你的每次读取——无论是 NIO 还是 IO——都是一个单独的系统调用,而映射文件是一个直接的内存访问(可能存在页面错误)。如果您的实际应用程序具有很大比例的本地化读取,您可能会受益于缓冲区缓存(可以是内存映射或堆上的)。如果你要遍历一个 TB 级的文件,那么磁盘 IO 将成为限制因素,甚至内存映射也无济于事。
      • 所以直接缓冲区和映射内存做同样的事情(避免内存复制),除了直接缓冲区可能会导致大量系统调用?对吗?
      • @MrROY - 我不明白你在问什么。但是不,直接的 ByteBuffer 不应该引起很多系统调用。 RandomAccessFile 可能。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多