【问题标题】:How to write large binary data fast in Java? [duplicate]如何在 Java 中快速写入大型二进制数据? [复制]
【发布时间】:2017-11-04 15:26:02
【问题描述】:

我正在编写一个 STL 文件,它包含一个 80 字节的标头、一个 4 字节的整数,然后是 50 个字节的记录,每个记录由浮点数和一个短整数组成。

使用 RandomAccessFile 我可以轻松写入数据,但速度非常慢。 这使用与 DataOutputStream 相同的接口。如果有一种简单的方法来缓冲数据输出流,我可以使用它,但烦人的部分是需要写出所有记录,最后计算三角形输出的数量并将该整数写入字节 81- 84.

直接但缓慢的方式,只专注于大部分工作,即编写每个方面:

public static void writeBinarySTL(Triangle t, RandomAccessFile d) throws IOException {
    d.writeFloat((float)t.normal.x);
    d.writeFloat((float)t.normal.y);
    d.writeFloat((float)t.normal.z);
    d.writeFloat((float)t.p1.x);
    d.writeFloat((float)t.p1.y);
    d.writeFloat((float)t.p1.z);
    d.writeFloat((float)t.p2.x);
    d.writeFloat((float)t.p2.y);
    d.writeFloat((float)t.p2.z);
    d.writeFloat((float)t.p3.x);
    d.writeFloat((float)t.p3.y);
    d.writeFloat((float)t.p3.z);
    d.writeShort(0);
}

有没有什么优雅的方法可以将这种二进制数据写入一个分块的、快速的 I/O 类?

我还想到 STL 文件格式应该是低字节优先,而 Java 可能是高字节优先。所以也许我所有的 writeFloats 都是徒劳的,我将不得不找到一个手动转换,以便它以 little-endian 形式出现?

如果必须,我愿意关闭文件,在随机访问文件的末尾重新打开,查找字节 81 并写入计数。

因此,此编辑是对应该有效的问题的两个答案的回应。首先是添加一个 BufferedWriter。结果出奇地快。我知道这台笔记本电脑是带有 SSD 的高端笔记本电脑,但我没想到会有这种性能,更不用说 Java 了。仅通过缓冲输出,0.5 秒内写入 96Mb 文件,1.5 秒内写入 196Mb。

为了看看 nio 是否会提供更高的性能,我尝试通过@sturcotte06 实现该解决方案。代码并没有尝试写标题,我只关注每个三角形的 50 字节记录。

public static void writeBinarySTL2(Shape3d s, String filename) {
    java.nio.file.Path filePath = Paths.get(filename);

    // Open a channel in write mode on your file.
    try (WritableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.CREATE)) {
        // Allocate a new buffer.
        ByteBuffer buf = ByteBuffer.allocate(50 * 1024);
        ArrayList<Triangle> triangles = s.triangles;
        // Write your triangle data to the buffer.
        for (int i = 0; i < triangles.size(); i += 1024) {
            for (int j = i; j < i + 1024; ++j)
                writeBinarySTL(triangles.get(j), buf);
            buf.flip(); // stop modifying buffer so it can be written to disk
            channel.write(buf);  // Write your buffer's data.
        }
        channel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

我尝试了 WRITE(文档说需要现有文件)和 CREATE,文档声称它们要么写入现有文件,要么创建一个新文件。

这两个选项都无法创建文件 Sphere902_bin2.stl

java.nio.file.NoSuchFileException: Sphere902_bin2.stl
sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:79)
        at 
sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97)
        at 
sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102)
        at 

sun.nio.fs.WindowsFileSystemProvider.newByteChannel(WindowsFileSystemProvider.java:230) 在 java.nio.file.Files.newByteChannel(Files.java:361) 在 java.nio.file.Files.newByteChannel(Files.java:407) 在 edu.stevens.scad.STL.writeBinarySTL2(STL.java:105)

我不相信写入字节的代码是相关的,但这是我想出的:

public static void writeBinarySTL(Triangle t, ByteBuffer buf) {
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.normal.x)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.normal.y)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.normal.y)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.p1.x)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.p1.y)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.p1.z)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.p2.x)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.p2.y)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.p2.z)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.p3.x)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.p3.y)));
    buf.putInt(Integer.reverseBytes(Float.floatToIntBits((float)t.p3.z)));
    buf.putShort((short)0);
}

这是一个 MWE,显示当写入超出缓冲区大小时代码无法正常工作:

package language;
import java.io.*;
import java.nio.*;
import java.nio.file.*;
import java.nio.channels.*;

public class FastWritenio {
    public static void writeUsingPrintWriter() throws IOException {
        PrintWriter pw = new PrintWriter(new FileWriter("test.txt"));
        pw.print("testing");
        pw.close();
    }
    public static void writeUsingnio(int numTrials, int bufferSize, int putsPer) throws IOException {
        String filename = "d:/test.dat";
        java.nio.file.Path filePath = Paths.get(filename);
        WritableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ);
        ByteBuffer buf = ByteBuffer.allocate(bufferSize);
        for (int t = 0; t < numTrials; ++t) {
            for (int i = 0; i < putsPer; i ++) {
                buf.putInt(i);
            }
            buf.flip(); // stop modifying buffer so it can be written to disk
            channel.write(buf);  // Write your buffer's data.
            // Without this line, it crashes:  buf.flip();
            // but with it this code is very slow.
        }
        channel.close();
    }
    public static void main(String[] args) throws IOException {
        writeUsingPrintWriter();
        long t0 = System.nanoTime();
        writeUsingnio(1024*256, 8*1024, 2048);
        System.out.println((System.nanoTime()-t0)*1e-9);

    }
}

【问题讨论】:

  • 如果有一种简单的方法可以缓冲数据输出流,您的意思是,像BufferedOutputStream? new DataOutputStream(new BufferedOutputStream(new FileOutputStream("out.dat")))
  • 好吧,你能缓冲一个 RandomAccessFile,还是我必须把文件写出来,作为一个 DataOutputStream,关闭,然后重新打开,寻找计数并写入?
  • 你当然可以这样做。或者你可以改为数三角形,然后写标题,然后写计数,然后写三角形。
  • 在所有答案中,速度提升来自于减少对操作系统的调用次数。从用户空间(应用程序运行的地方)到内核空间的上下文切换非常昂贵。 RandomAccessFile(以及FileOutputStream)为每个方法调用调用操作系统。 BufferedOutputStream 在调用操作系统之前首先累积可配置的字节数(默认为 8kb)。对于 4 字节的写入,上下文切换次数减少了 2048 倍。
  • 这并不完全正确。如果您写入奇数字节,您将写入同一个块两次。因此,例如,磁盘块大小为 4k,缓冲区大小为 4k-1,您将每个块写入两次。在所有条件相同的情况下,更大的缓冲区在某种程度上更好,但是如果你制作一个巨大的缓冲区,你甚至不会写入磁盘直到稍后,所以对于多线程和缓存问题,还有一些其他的事情需要考虑.

标签: java nio binary-data


【解决方案1】:

使用 nio 的 ByteBuffer 类:

public static void writeBinarySTL(Triangle t, ByteBuffer buf) {
    buf.putFloat((float)t.normal.x);
    // ...
}

// Create a new path to your file on the default file system.
Path filePath = Paths.get("file.txt");

// Open a channel in write mode on your file.
try (WritableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.WRITE)) {
    // Allocate a new buffer.
    ByteBuffer buf = ByteBuffer.allocate(8192);

    // Write your triangle data to the buffer.
    writeBinarySTL(t, buf);

    // Flip from write mode to read mode.
    buf.flip();

    // Write your buffer's data.
    channel.write(buf);
}

ByteBuffer tutorial

ByteBuffer javadoc

WriteableByteChannel javadoc

Files javadoc

编辑:

public static boolean tryWriteBinarySTL(Triangle t, ByteBuffer buf) {
    final int triangleBytes = 50; // set this.
    if (buf.remaining() < triangleBytes) {
       return false;
    }

    buf.putFloat((float)t.normal.x);
    // ...
    return true;
}

// Create a new path to your file on the default file system.
Path filePath = Paths.get("file.txt");

// Open a channel in write mode on your file.
try (WritableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.WRITE)) {
    // Allocate a new buffer.
    ByteBuffer buf = ByteBuffer.allocate(8192);

    // Write your triangle data to the buffer.
    for (Triangle triangle : triangles) {
        while (!tryWriteBinarySTL(triangle, buf) ) {
            // Flush buffer.
            buf.flip();
            while (buf.hasRemaining()) {
                channel.write(buf);
            }

            buf.flip();
        }
    }

    // Write remaining.
    buf.flip();
    channel.write(buf);
}

【讨论】:

  • ByteBuffer 的文档非常不清楚。我可以使用 putInt() 写入整数,使用 putShort() 写入一个短整数,但是如果写入的值不能均匀地适合缓冲区大小,我是否继续写入缓冲区(即它是否自动刷新?我什么时候有翻转?我为什么要翻转?我只是想基本冲洗......
  • 缓冲区不可刷新;您将内存刷新到磁盘。缓冲区在内存中。如果缓冲区已满,缓冲区将被抛出,并且无法将您的数据结构放入其中。缓冲区本身对文件没有任何作用;它需要写入文件通道。以我为榜样。
  • Flip 用于在缓冲区的读写模式之间切换。基本上,数据源将写入缓冲区,然后将其翻转以供数据目标读取。翻转时,缓冲区存储缓冲区的当前大小并对其进行限制;因此它将包含正确的字节数。
  • 磁盘将有效地写入块大小的倍数。当我一次写入 50 个字节时,这段代码如何处理它,这永远不会是块大小的倍数?
  • 这与问题有什么关系?操作系统处理磁盘;这不是你的责任。您正在尝试进行微优化。你现在需要优化的是系统调用的数量。用数据填充缓冲区,无论是标头还是有效负载。使用填充的缓冲区调用 write。如果您的数据少于一个缓冲区,那么它永远不会高效,但它不需要如此,因为您的数据很少。在操作系统级别和磁盘级别都有缓冲,不要试图比今天的系统更智能;你最终会得到更差的性能和更复杂的代码。
【解决方案2】:

如果有简单的缓冲方法

有:

DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(...));

【讨论】:

  • 这看起来真的很棒。我将把这个添加到我的问题中。我想了解 nio 解决方案,但是有了这个解决方案,我得到了 196Mb/1.5 秒的疯狂速率
【解决方案3】:

DataOutputStream 不是一个特别快速的解决方案。创建一个普通的旧FileOutputStream,在其周围放置一个BufferedOutputStream,然后编写自己的代码将数据写入字节。 FloatDouble 类为此提供了帮助函数。 doubleToLongBits,例如。

如果这对您来说还不够快,请阅读 NIO2。

【讨论】:

  • 它至少和你自己的代码一样快,而且已经可以了。
  • 我不同意阅读源代码。但我赞成缓冲答案。
  • 就使用流而言,我在 DataOutputStream 中没有看到特别的问题。它不使用多字节写入,但给出了一个无论如何都会优化的底层 BufferrdOutputSzream。如果你愿意,你可以实现 writeInt 和 writeLong,这也被 float 和 double 使用。我不确定它是否有帮助,因为数据需要复制两次。我不认为你可以更快地实现它。
  • @bmargulies 我也读过源代码,这和我写的差不多。如果您有具体建议,您应该提供。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-07-18
  • 2014-10-05
  • 2010-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多