6

デフォルトの ByteArrayOutputStream はかなり無駄な実装のようで、これには特定の理由があるのではないかと考えていました。まず、バックエンドに 1 つの固定配列を保持します。それがいっぱいになると、新しい配列が作成され、古い配列がそこにコピーされます (より多くのメモリ + より多くのオーバーヘッド)。次に、 toByteArray() を実行すると、実際に配列が再度コピーされます。

バイト バッファは優れていますが、サイズも固定されています。単一の配列でいくつかのバッファを提供するだけで、それ以上のものはありません。

1 つ以上のバッキング配列を使用するクラスを作成することに興味があるかどうか (または、既に存在する場合は、それを教えてください) を考えていました。展開するたびに配列を複製する代わりに、新しいバッキング配列を追加するだけです。読み取るには、inputstream のようなインターフェイスを簡単に作成できますが、書き込み用に outputstream のようなインターフェイスを公開できます。

そのようなものがすでに存在するかどうかについてのフィードバックと、そうでない場合: なぜですか? 私が見ていない欠点はありますか?

4

4 に答える 4

2

これは、特に大きなデータの場合、実際には素晴らしいアイデアです。

heap に巨大な配列を割り当てると、連続した空きメモリを割り当てる必要があるため、すぐにメモリの問題が発生する可能性があります。以前、10 ~ 50MB のサイズのバイト配列を頻繁に割り当てて s に遭遇したことがありましたが、これは使用可能なメモリが少なすぎるためではなく (通常は 90% または 900MB の空き容量がありました)、ヒープの断片化OutOfMemoryException原因でした。この配列で使用できる単一の連続したメモリ ブロックではありませんでした。

最終的に、チェーン化された(リスト)小さな配列Blobのチャンクとしてデータを内部的に保存するクラスを作成しました。配列のサイズは固定されており (迅速な検索に不可欠であるため、関連する配列と特定のインデックスのオフセットをすばやく計算できます)、この Blob のクラスを作成します。後で、ディスクとの間でスワップできるように拡張しました。InputStreamOutputStream

  • 欠点?少し簡単なプログラミング作業を除けば、何もありません。
  • 利点?大規模なデータをメモリに効率的に格納し、ヒープの断片化の問題がなくなります。

私はあなたにそれを試してみることを勧めることしかできません!

于 2013-10-04T07:46:24.747 に答える
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 よりも書き込みの方がわずかに速い (5 ~ 20% の増加)。

すぐにまとめたので、バグを見つけたら教えてください。

編集

読み込まれた配列がデフォルトで gc 用に解放される機能を追加

于 2013-10-04T08:12:34.607 に答える
0

メリットがよくわかるようです。単一のバッファーと比較したバッファーのリストの欠点は次のとおりです。

  • バッファーが固定サイズの場合、n バイトを書き込むために O(n) メモリ割り当てが必要です。ByteArrayOutputStream は O(log n) になります。これは、バッファーが指数関数的に増加するためです。
  • 実装はより複雑です。アクティブなバッファーを追跡する必要があり、書き込みの途中でバッファーを切り替える必要がある場合があります (設計によって異なります)。
  • 読み込み時のバッファ切り替えはキャッシュミス

アプリケーションにとって意味がある場合は、そのようなデータ構造を自由に記述できます。

于 2013-10-04T07:55:04.593 に答える
0

C++ 標準ライブラリには、ベクトル クラス (Java ArrayList のような) と deque クラス (別の List のようなクラス) の両方があります。後者は効率的なプリペンドと効率的なアペンドを提供します。私が見た実装では、固定長配列のブロックのリストが維持されていました。だから、あなたが興味を持っているケースのようなものです。したがって、これは確かに可能です。

欠点は、コードの複雑さが大幅に増加することです。toByteArray メソッドがフラグメントからデータを収集することで、JRE の実装を変更して、あなたが提案したことを実行できると思います。ただし、単純な実装は非常に高速であるため、これを行うことは非常に優先度が低くなります。IO を実行するコードは、読み取りと書き込みが低速な操作であり、ブロックされる可能性があると想定する必要があります。ByteArrayOutputStream は、真の外部 IO ではなくメモリ操作で実行されるため、非常に高速です。これらのバイト配列をコピーすると、外部 IO よりもはるかに高速になる可能性があります。現在の実装の欠点は、大きな出力ストリームに使用すると、大きなガベージ配列が作成されることです。ただし、クラスの使用例は小さなストリーム用です。大きな出力ストリームのバイトを一時的に保存したい場合、一時ファイルを使用する必要があります。したがって、提案の複雑さは、実際にはあまり役に立たないでしょう

于 2013-10-04T07:49:33.227 に答える