在Java中,将整数数组写入文件的最快方法是什么?

13

正如标题所说,我正在寻找将整数数组写入文件的最快方式。这些数组的大小会有所不同,实际上可能包含2500到2500万个整数。

以下是我目前使用的代码:

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%。我没有进行足够严格的测试来确认这一点。

是否有任何技巧可以加快文件写操作,或者有关Java IO写性能最佳实践的相关指南?


2
你希望文件内容以什么样的格式呈现? - Karl Knechtel
自己内联代码将使未热身的代码更快。但是,如果您运行测试5-10秒钟,您将看到是否有真正的改进。(因为JVM会为您执行此操作) - Peter Lawrey
@Karl 只是一系列没有格式的整数。 - Ollie Glass
6个回答

26

我看了三种选项:

  1. 使用DataOutputStream
  2. 使用ObjectOutputStream(对于实现了Serializable接口的对象,如int[]);以及
  3. 使用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时遇到了一个错误:java.nio.channels.NonReadableChannelException。你知道为什么吗? - Ollie Glass
2
我使用了@dacwe的方法来写入FileChannel,修改后的代码在这里http://pastebin.com/HhpcS7HX。 - Ollie Glass
我得到了同样的异常,有什么想法吗? - steveh
1
问题在于代码试图从一个只写对象中读取和写入;FileOutputStream仅支持写入。相反,代码应该使用以“rw”打开的RandomAccessFile。 - MikeB
使用缓冲层会提高这些数字吗? - Thorbjørn Ravn Andersen
1
另一个小错误是,在DataOutputStream中,out.write(anInt)写入的是一个字节而不是整数。使用整数时性能可能会更差。另一方面,您应该将FileOutputStream包装在BufferedOutputStream中。 - Florian F

7
我会使用nio包中的FileChannel和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;
    }
}

你能否使用直接内存缓冲区进行重新测试?这样应该会使写入速度更快(因为它必须复制到直接缓冲区)。 - Peter Lawrey
也可以尝试使用64K缓冲区大小的BufferOutputStream。 - Peter Lawrey
1
谢谢,使用FileChannel方法更快。 - Ollie Glass

5

基准测试应该定期重复,不是吗? :) 在修复一些错误并添加自己的写作变体后,在运行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

每个测试都写了一个800mb的文件。未缓冲的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问题,因此不总是使用的正确工具。 Java.nio.MappedByteBuffer使用时如何避免OutOfMemory

这里是我修改后的基准代码版本: https://gist.github.com/bjourne/53b7eabc6edea27ffb042e7816b7830b


3

3
你可以对写入int[]的主要改进有两种方法:
  • 增加缓冲区大小。大多数流的大小是正确的,但使用更大的缓冲区可以加快文件访问速度。这可能会带来10-20%的提高。

  • 使用NIO和直接缓冲区。这使您可以写入32位值而无需转换为字节。这可能会带来5%的提高。

顺便说一下:您应该能够每秒写入至少1000万个int值。通过磁盘缓存,您可以将其增加到每秒200百万个。


0

数组是可序列化的 - 你不能只使用writer.writeObject(data);吗?这肯定比单独的writeInt调用更快。

如果您对输出数据格式有其他要求,而不仅仅是检索到int[]中,那就是另一个问题了。


1
writeObject有很大的开销,并在最后使用writeInt。编写对象的友好方式更好,我怀疑在大多数情况下是更好的选择。 - Peter Lawrey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接