Проблема производительности Java ByteBuffer - PullRequest
16 голосов
/ 12 октября 2011

При обработке нескольких гигабайтных файлов я заметил нечто странное: кажется, что чтение из файла с использованием файлового канала в повторно используемый объект ByteBuffer, выделенный с помощью allocateDirect, намного медленнее, чем чтение из MappedByteBuffer, на самом деле это даже медленнее, чем чтение в байтовые массивы с использованием регулярных вызовов чтения!

Я ожидал, что это будет (почти) так же быстро, как чтение из mappedbytebuffers, поскольку мой ByteBuffer выделен с помощью allocateDirect, поэтому чтение должно закончиться непосредственно в моем байтовом буфере без каких-либо промежуточных копий.

Мой вопрос сейчас такой: что я делаю не так? Или bytebuffer + filechannel действительно медленнее, чем обычный io / mmap?

В приведенном ниже примере кода я также добавил некоторый код, который преобразует то, что читается, в длинные значения, поскольку это то, чем постоянно занимается мой настоящий код. Я ожидаю, что метод getLong () ByteBuffer намного быстрее, чем мой собственный shuffeler байтов.

Тест-результаты: Mmap: 3,828 байтбуфер: 55,097 обычный ввод / вывод: 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. Спасибо всем за ваши предложения.

Ответы [ 4 ]

10 голосов
/ 12 октября 2011

Я полагаю, что вы просто делаете микрооптимизацию, , которая может просто не имеет значения (www.codinghorror.com) .

Ниже приведена версия с увеличенным буфером и удаленными seek / setPosition вызовами.

  • Когда я включаю «собственное упорядочение байтов» (что на самом деле небезопасно, если машина использует другое соглашение о порядке байтов):
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);
    }
}
5 голосов
/ 13 октября 2011

Чтение в прямой байтовый буфер происходит быстрее, но выгрузка данных из него в JVM происходит медленнее.Прямой байтовый буфер предназначен для случаев, когда вы просто копируете данные, фактически не просматривая их в коде Java.Тогда ему вообще не нужно пересекать границу native-> JVM, поэтому это быстрее, чем, например, использовать массив byte [] или обычный ByteBuffer, где данные должны будут пересекать эту границу дважды в процессе копирования.

2 голосов
/ 12 октября 2011

Если у вас есть цикл, который повторяется более 10000 раз, он может инициировать компиляцию всего метода в собственный код.Однако ваши последующие циклы не были запущены и не могут быть оптимизированы до такой же степени.Чтобы избежать этой проблемы, поместите каждый цикл в свой метод и повторите его.

Кроме того, вы можете установить порядок для ByteBuffer в порядке (ByteOrder.nativeOrder ()), чтобы избежать перестановки всех байтов.примерно, когда вы делаете getLong и читаете более 24 байтов за раз.(Так как чтение очень маленьких порций приводит к гораздо большему количеству системных вызовов) Попробуйте прочитать 32 * 1024 байта за раз.

Я также попробую getLong на MappedByteBuffer с собственным порядком байтов.Это, вероятно, будет самым быстрым.

0 голосов
/ 12 октября 2011

A MappedByteBuffer всегда будет самым быстрым, потому что операционная система связывает дисковый буфер уровня ОС с пространством памяти вашего процесса.Для сравнения, чтение в выделенном прямом буфере сначала загружает блок в буфер ОС, а затем копирует содержимое буфера ОС в выделенный внутрипроцессный буфер.

Ваш тестовый код также делает очень мало(24 байта) читает.Если ваше реальное приложение делает то же самое, то при сопоставлении файла вы получите еще больший прирост производительности, потому что каждое чтение - это отдельный вызов ядра.Вы должны несколько раз увидеть производительность при отображении.

Что касается прямого буфера, который медленнее, чем читает java.io: вы не даете никаких чисел, но я бы ожидал небольшую деградацию, поскольку getLong() звонки должны пересекать границу JNI.

...