6

默认的 ByteArrayOutputStream 似乎是一个相当浪费的实现,我想知道是否有任何具体原因。首先,它在后端保留 1 个固定数组。如果已满,它会创建一个新数组并将旧数组复制到其中(更多内存 + 更多开销)。然后,如果您执行 toByteArray() 它实际上会再次复制数组。

字节缓冲区很好,但大小也是固定的,它们仅在单个数组上提供一些,仅此而已。

我想知道创建一个使用一个或多个支持数组的类(或者如果它已经存在,请指向它)是否会很有趣。它不是每次都复制数组来扩展,而是添加一个新的支持数组。要阅读,您可以轻松创建像 inputstream 这样的接口,同时可以公开像 outputstream 这样的接口以进行写入

关于这样的事情是否已经存在以及如果不存在的任何反馈:为什么?它有一些我没有看到的缺点吗?

4

4 回答 4

2

这实际上是一个好主意,尤其是对于大数据。

在堆上分配大型数组时,您可能会很快遇到内存问题,因为它们需要分配连续的空闲内存。我们曾经有过这样的情况,我们经常分配 10-50MB 大小的字节数组,跑到OutOfMemoryExceptions,不是因为可用内存太少(我们通常有 90%,或者 900MB 空闲),而是因为那里的堆碎片不是可以用于该数组的单个连续内存块。

我们最终创建了一个Blob类,该类在内部将数据存储为链式(列表)较小数组的块。数组具有固定大小(对于快速查找至关重要,因此您可以快速计算所涉及的数组和给定索引的偏移量),我们为此 Blob创建InputStream和类。OutputStream后来我们将其扩展为可与磁盘交换。

  • 缺点?没有,除了一些简单的编程工作。
  • 好处?在内存中高效存储大数据,不再有堆碎片问题。

我只能鼓励你试一试!

于 2013-10-04T07:46:24.747 回答
0

看起来你已经知道好处了。与单个缓冲区相比,缓冲区列表的缺点包括:

  • 如果缓冲区是固定大小的,则需要 O(n) 内存分配来写入 n 个字节,ByteArrayOutputStream 使 O(log n) 因为缓冲区呈指数增长
  • 实现更复杂:您必须跟踪活动缓冲区,可能需要在写入过程中切换缓冲区(取决于设计)
  • 读取时切换缓冲区是缓存未命中

如果它对您的应用程序有意义,您可以自由编写这样的数据结构

于 2013-10-04T07:55:04.593 回答
0

由于似乎没有真正的实现,我很快编写了一个初始实现来测试速度:

public class Buffer {

    private int size;

    private int writeIndex, writeOffset,
        readIndex, readOffset;

    private List<byte[]> backingArrays = new ArrayList<byte[]>();

    public Buffer() {
        this(10240);
    }

    public Buffer(int size) {
        this.size = size;
    }

    public int read(byte [] bytes) {
        return read(bytes, 0, bytes.length);
    }

    public int read(byte [] bytes, int offset, int length) {
        int read = 0;
        while(length > 0) {
            byte [] input = getInput();
            // no more data
            if (input == null) {
                if (read == 0)
                    return -1;
                else
                    return read;
            }
            int readLength = Math.min(length, (readIndex == writeIndex ? writeOffset : size) - readOffset);
            System.arraycopy(input, readOffset, bytes, offset, readLength);
            length -= readLength;
            offset += readLength;
            readOffset += readLength;
            read += readLength;
        }
        return read;
    }

    public void write(byte [] bytes) {
        write(bytes, 0, bytes.length);
    }

    public void write(byte [] bytes, int offset, int length) {
        while (length > 0) {
            byte [] output = getOutput();
            int writeLength = Math.min(length, output.length - writeOffset);
            System.arraycopy(bytes, offset, output, writeOffset, writeLength); 
            length -= writeLength;
            offset += writeLength;
            writeOffset += writeLength;
        }
    }

    private byte[] getOutput() {
        // if we have filled an array, move to the next one
        if (writeOffset >= size) {
            writeIndex++;
            writeOffset = 0;
        }
        // create it if it doesn't exist yet
        if (backingArrays.size() <= writeIndex)
            backingArrays.add(new byte[size]);

        return backingArrays.get(writeIndex);
    }

    private byte [] getInput() {
        // nothing written yet
        if (backingArrays.size() == 0)
            return null;

        if (readOffset >= size) {
            readIndex++;
            readOffset = 0;
        }
        // can not read past where it is written
        if (readIndex > writeIndex || (readIndex == writeIndex && readOffset >= writeOffset))
            return null;
        else
            return backingArrays.get(readIndex);
    }

    public long size() {
        return (long) size * (long) writeIndex + writeOffset;
    }
}

我正在通过复制一个 36 兆的文件来测试它。很多当然取决于文件交互,但一般来说,读取速度似乎比 bytearrayinputstream 快 40% 写入速度略快(徘徊在 5-20% 左右)

我很快把它放在一起,所以如果你发现任何错误,请告诉我。

编辑

添加了一个功能,默认情况下已读取的数组为 gc 释放

于 2013-10-04T08:12:34.607 回答
0

C++ 标准库既有一个向量类(如 Java ArrayList)和一个双端队列类(另一个类似 List 的类)。后者提供了高效的前置和高效的附加。我见过的实现维护了一个固定长度数组块的列表。所以,有点像你感兴趣的案例。所以,这当然是可能的。

缺点是代码的复杂性大大增加。我猜想 JRE 中的实现可以按照您的建议进行更改,使用 toByteArray 方法从片段中收集数据。但是这样做的优先级很低,因为简单的实现速度非常快。任何做 IO 的代码都应该假设读写是慢速操作,可能会阻塞。相反,ByteArrayOutputStream 非常快,因为它在内存操作而不是真正的外部 IO 中执行。复制这些字节数组可能比外部 IO 快得多。当前实现的缺点是在用于大型输出流时会创建大型垃圾数组。但是该类的用例是针对小流的;如果要临时存储大输出流的字节,你应该使用一个临时文件。因此,您的提案的复杂性在实践中可能没有多大帮助

于 2013-10-04T07:49:33.227 回答