Самый быстрый способ написать массив целых чисел в файл на Java?

Как говорится в названии, я ищу самый быстрый способ записи целочисленных массивов в файлы. Массивы будут различаться по размеру и будут реально содержать от 2500 до 25 000 000 ints.

Вот код, который я сейчас использую:

DataOutputStream writer = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filename)));

for (int d : data)
  writer.writeInt(d);

Учитывая, что DataOutputStream имеет метод записи массивов байтов, я попытался преобразовать массив int в байтовый массив следующим образом:

private static byte[] integersToBytes(int[] values) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    DataOutputStream dos = new DataOutputStream(baos);
    for (int i = 0; i < values.length; ++i) {
        dos.writeInt(values[i]);
    }

    return baos.toByteArray();
}

и вот так:

private static byte[] integersToBytes2(int[] src) {
    int srcLength = src.length;
    byte[] dst = new byte[srcLength << 2];

    for (int i = 0; i < srcLength; i++) {
        int x = src[i];
        int j = i << 2;
        dst[j++] = (byte) ((x >>> 0) & 0xff);
        dst[j++] = (byte) ((x >>> 8) & 0xff);
        dst[j++] = (byte) ((x >>> 16) & 0xff);
        dst[j++] = (byte) ((x >>> 24) & 0xff);
    }
    return dst;
}

Оба, похоже, дают небольшое увеличение скорости, около 5%. Я не проверял их достаточно строго, чтобы подтвердить это.

Существуют ли какие-либо методы, которые ускорят эту операцию записи файла, или соответствующие руководства по наилучшей практике для производительности записи в IO для Java?

Ответ 1

Я рассмотрел три варианта:

  • Использование DataOutputStream;
  • Использование ObjectOutputStream (для Serializable объектов, для которых int[]); и
  • Использование FileChannel.

Результаты

DataOutputStream wrote 1,000,000 ints in 3,159.716 ms
ObjectOutputStream wrote 1,000,000 ints in 295.602 ms
FileChannel wrote 1,000,000 ints in 110.094 ms

Таким образом, версия NIO является самой быстрой. Он также имеет преимущество в разрешении редактирования, то есть вы можете легко изменить один int, тогда как ObjectOutputStream потребует прочтения всего массива, изменения его и записи его в файл.

Код следует:

private static final int NUM_INTS = 1000000;

interface IntWriter {
  void write(int[] ints);
}

public static void main(String[] args) {
  int[] ints = new int[NUM_INTS];
  Random r = new Random();
  for (int i=0; i<NUM_INTS; i++) {
    ints[i] = r.nextInt();
  }
  time("DataOutputStream", new IntWriter() {
    public void write(int[] ints) {
      storeDO(ints);
    }
  }, ints);
  time("ObjectOutputStream", new IntWriter() {
    public void write(int[] ints) {
      storeOO(ints);
    }
  }, ints);
  time("FileChannel", new IntWriter() {
    public void write(int[] ints) {
      storeFC(ints);
    }
  }, ints);
}

private static void time(String name, IntWriter writer, int[] ints) {
  long start = System.nanoTime();
  writer.write(ints);
  long end = System.nanoTime();
  double ms = (end - start) / 1000000d;
  System.out.printf("%s wrote %,d ints in %,.3f ms%n", name, ints.length, ms);
}

private static void storeOO(int[] ints) {
  ObjectOutputStream out = null;
  try {
    out = new ObjectOutputStream(new FileOutputStream("object.out"));
    out.writeObject(ints);
  } catch (IOException e) {
    throw new RuntimeException(e);
  } finally {
    safeClose(out);
  }
}

private static void storeDO(int[] ints) {
  DataOutputStream out = null;
  try {
    out = new DataOutputStream(new FileOutputStream("data.out"));
    for (int anInt : ints) {
      out.write(anInt);
    }
  } catch (IOException e) {
    throw new RuntimeException(e);
  } finally {
    safeClose(out);
  }
}

private static void storeFC(int[] ints) {
  FileOutputStream out = null;
  try {
    out = new FileOutputStream("fc.out");
    FileChannel file = out.getChannel();
    ByteBuffer buf = file.map(FileChannel.MapMode.READ_WRITE, 0, 4 * ints.length);
    for (int i : ints) {
      buf.putInt(i);
    }
    file.close();
  } catch (IOException e) {
    throw new RuntimeException(e);
  } finally {
    safeClose(out);
  }
}

private static void safeClose(OutputStream out) {
  try {
    if (out != null) {
      out.close();
    }
  } catch (IOException e) {
    // do nothing
  }
}

Ответ 2

Я использовал бы FileChannel из пакета nio и ByteBuffer. Этот подход кажется (на моем компьютере) дает в 2-4 раза лучшую производительность записи:

Выход из программы:

normal time: 2555
faster time: 765

Это программа:

public class Test {

    public static void main(String[] args) throws IOException {

        // create a test buffer
        ByteBuffer buffer = createBuffer();

        long start = System.currentTimeMillis();
        {
            // do the first test (the normal way of writing files)
            normalToFile(new File("first"), buffer.asIntBuffer());
        }
        long middle = System.currentTimeMillis(); 
        {
            // use the faster nio stuff
            fasterToFile(new File("second"), buffer);
        }
        long done = System.currentTimeMillis();

        // print the result
        System.out.println("normal time: " + (middle - start));
        System.out.println("faster time: " + (done - middle));
    }

    private static void fasterToFile(File file, ByteBuffer buffer) 
    throws IOException {

        FileChannel fc = null;

        try {

            fc = new FileOutputStream(file).getChannel();
            fc.write(buffer);

        } finally {

            if (fc != null)
                fc.close();

            buffer.rewind();
        }
    }

    private static void normalToFile(File file, IntBuffer buffer) 
    throws IOException {

        DataOutputStream writer = null;

        try {
            writer = 
                new DataOutputStream(new BufferedOutputStream(
                        new FileOutputStream(file)));

            while (buffer.hasRemaining())
                writer.writeInt(buffer.get());

        } finally {
            if (writer != null)
                writer.close();

            buffer.rewind();
        }
    }

    private static ByteBuffer createBuffer() {
        ByteBuffer buffer = ByteBuffer.allocate(4 * 25000000);
        Random r = new Random(1);

        while (buffer.hasRemaining()) 
            buffer.putInt(r.nextInt());

        buffer.rewind();

        return buffer;
    }
}

Ответ 4

Основное улучшение, которое вы можете получить для записи int [], - либо:

  • увеличить размер буфера. Размер подходит для большинства потоков, но доступ к файлам может быть быстрее с большим буфером. Это может привести к 10-20% улучшению.

  • Используйте NIO и прямой буфер. Это позволяет записывать 32-битные значения без преобразования в байты. Это может привести к 5% -ному улучшению.

BTW: вы должны иметь возможность писать не менее 10 миллионов значений int в секунду. С кэшированием диска вы увеличиваете это до 200 миллионов в секунду.

Ответ 5

Массив Serializable - не можете ли вы просто использовать writer.writeObject(data);? Это определенно будет быстрее, чем индивидуальные вызовы writeInt.

Если у вас есть другие требования к формату выходных данных, чем поиск в int[], это другой вопрос.

Ответ 6

Тесты должны повторяться время от времени, не так ли? :) После исправления некоторых ошибок и добавления моего собственного варианта записи, вот результаты, которые я получаю при запуске теста на ASUS ZenBook UX305 под управлением Windows 10 (время указывается в секундах):

Running tests... 0 1 2
Buffered DataOutputStream           8,14      8,46      8,30
FileChannel alt2                    1,55      1,18      1,12
ObjectOutputStream                  9,60     10,41     11,68
FileChannel                         1,49      1,20      1,21
FileChannel alt                     5,49      4,58      4,66

И вот результаты, запущенные на том же компьютере, но с Arch Linux и переключенным порядком методов записи:

Running tests... 0 1 2
Buffered DataOutputStream          31,16      6,29      7,26
FileChannel                         1,07      0,83      0,82
FileChannel alt2                    1,25      1,71      1,42
ObjectOutputStream                  3,47      5,39      4,40
FileChannel alt                     2,70      3,27      3,46

Каждый тест записал файл 800 МБ. Небуферизованный DataOutputStream занял много времени, поэтому я исключил его из теста.

Как видно, запись с использованием файлового канала по-прежнему превосходит все остальные методы, но очень важно, отображается ли байтовый буфер в памяти или нет. Без отображения памяти запись файла в канал заняла 3-5 секунд:

var bb = ByteBuffer.allocate(4 * ints.length);
for (int i : ints)
    bb.putInt(i);
bb.flip();
try (var fc = new FileOutputStream("fcalt.out").getChannel()) {
    fc.write(bb);
}

С отображением памяти время было сокращено до 0,8-1,5 секунд:

try (var fc = new RandomAccessFile("fcalt2.out", "rw").getChannel()) {
    var bb = fc.map(READ_WRITE, 0, 4 * ints.length);
    bb.asIntBuffer().put(ints);
}

Но обратите внимание, что результаты зависят от порядка. Особенно это касается Linux. Похоже, что методы с отображением в памяти не записывают данные полностью, а выгружают запрос задания в ОС и возвращают его до завершения. Желательно ли такое поведение или нет, зависит от ситуации.

Отображение памяти также может привести к проблемам OutOfMemory, поэтому это не всегда правильный инструмент для использования. Запрет OutOfMemory при использовании java.nio.MappedByteBuffer.

Вот моя версия кода теста: https://gist.github.com/bjourne/53b7eabc6edea27ffb042e7816b7830b