web-dev-qa-db-ja.com

C#メモリストリームがこれほど多くのメモリを予約するのはなぜですか?

私たちのソフトウェアは、GZipStreamからデータを読み取るMemoryStreamを介して特定のバイトデータを解凍しています。これらのデータは4KBのブロックに解凍され、別のMemoryStreamに書き込まれます。

プロセスが割り当てるメモリは、実際の解凍されたデータよりもはるかに高いことがわかりました。

例:2,425,536バイトの圧縮バイト配列は23,050,718バイトに解凍されます。使用するメモリプロファイラーは、メソッドMemoryStream.set_Capacity(Int32 value)が67,104,936バイトを割り当てたことを示しています。これは、予約済みメモリと実際に書き込まれたメモリの間で2.9倍です。

注:MemoryStream.set_CapacityMemoryStream.EnsureCapacityから呼び出され、それ自体は関数のMemoryStream.Writeから呼び出されます。

MemoryStreamは、4KBのブロックしか追加しないのに、なぜこれほど多くの容量を予約するのですか?

データを解凍するコードスニペットは次のとおりです。

private byte[] Decompress(byte[] data)
{
    using (MemoryStream compressedStream = new MemoryStream(data))
    using (GZipStream zipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
    using (MemoryStream resultStream = new MemoryStream())
    {
        byte[] buffer = new byte[4096];
        int iCount = 0;

        while ((iCount = zipStream.Read(buffer, 0, buffer.Length)) > 0)
        {
            resultStream.Write(buffer, 0, iCount);
        }
        return resultStream.ToArray();
    }
}

注:関連する場合、これはシステム構成です。

  • Windows XP 32ビット、
  • .NET 3.5
  • Visual Studio2008でコンパイル
30
Tim Meyer

これはアルゴリズムです 容量を拡張する方法について。

public override void Write(byte[] buffer, int offset, int count) {

    //... Removed Error checking for example

    int i = _position + count;
    // Check for overflow
    if (i < 0)
        throw new IOException(Environment.GetResourceString("IO.IO_StreamTooLong"));

    if (i > _length) {
        bool mustZero = _position > _length;
        if (i > _capacity) {
            bool allocatedNewArray = EnsureCapacity(i);
            if (allocatedNewArray)
                mustZero = false;
        }
        if (mustZero)
            Array.Clear(_buffer, _length, i - _length);
        _length = i;
    }

    //... 
}

private bool EnsureCapacity(int value) {
    // Check for overflow
    if (value < 0)
        throw new IOException(Environment.GetResourceString("IO.IO_StreamTooLong"));
    if (value > _capacity) {
        int newCapacity = value;
        if (newCapacity < 256)
            newCapacity = 256;
        if (newCapacity < _capacity * 2)
            newCapacity = _capacity * 2;
        Capacity = newCapacity;
        return true;
    }
    return false;
}

public virtual int Capacity 
{
    //...

    set {
         //...

        // MemoryStream has this invariant: _Origin > 0 => !expandable (see ctors)
        if (_expandable && value != _capacity) {
            if (value > 0) {
                byte[] newBuffer = new byte[value];
                if (_length > 0) Buffer.InternalBlockCopy(_buffer, 0, newBuffer, 0, _length);
                _buffer = newBuffer;
            }
            else {
                _buffer = null;
            }
            _capacity = value;
        }
    }
}

したがって、容量制限に達するたびに、容量のサイズが2倍になります。これを行う理由は、大きな配列ではBuffer.InternalBlockCopy操作が遅いため、書き込み呼び出しごとに頻繁にサイズを変更する必要がある場合、パフォーマンスが大幅に低下するためです。

パフォーマンスを向上させるためにできることは、初期容量を少なくとも圧縮アレイのサイズに設定してから、サイズを2.0よりも小さい係数で増やして量を減らすことです。使用しているメモリ。

const double ResizeFactor = 1.25;

private byte[] Decompress(byte[] data)
{
    using (MemoryStream compressedStream = new MemoryStream(data))
    using (GZipStream zipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
    using (MemoryStream resultStream = new MemoryStream(data.Length * ResizeFactor)) //Set the initial size to be the same as the compressed size + 25%.
    {
        byte[] buffer = new byte[4096];
        int iCount = 0;

        while ((iCount = zipStream.Read(buffer, 0, buffer.Length)) > 0)
        {
            if(resultStream.Capacity < resultStream.Length + iCount)
               resultStream.Capacity = resultStream.Capacity * ResizeFactor; //Resize to 125% instead of 200%

            resultStream.Write(buffer, 0, iCount);
        }
        return resultStream.ToArray();
    }
}

必要に応じて、現在の圧縮率に基づいたサイズ変更など、さらに高度なアルゴリズムを実行できます。

const double MinResizeFactor = 1.05;

private byte[] Decompress(byte[] data)
{
    using (MemoryStream compressedStream = new MemoryStream(data))
    using (GZipStream zipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
    using (MemoryStream resultStream = new MemoryStream(data.Length * MinResizeFactor)) //Set the initial size to be the same as the compressed size + the minimum resize factor.
    {
        byte[] buffer = new byte[4096];
        int iCount = 0;

        while ((iCount = zipStream.Read(buffer, 0, buffer.Length)) > 0)
        {
            if(resultStream.Capacity < resultStream.Length + iCount)
            {
               double sizeRatio = ((double)resultStream.Position + iCount) / (compressedStream.Position + 1); //The +1 is to prevent divide by 0 errors, it may not be necessary in practice.

               //Resize to minimum resize factor of the current capacity or the 
               // compressed stream length times the compression ratio + min resize 
               // factor, whichever is larger.
               resultStream.Capacity =  Math.Max(resultStream.Capacity * MinResizeFactor, 
                                                 (sizeRatio + (MinResizeFactor - 1)) * compressedStream.Length);
             }

            resultStream.Write(buffer, 0, iCount);
        }
        return resultStream.ToArray();
    }
}
44

MemoryStreamは、スペースが不足すると内部バッファーを2倍にします。これは2倍の無駄につながる可能性があります。なぜそれ以上見ているのかわかりません。しかし、この基本的な動作は予想されます。

この動作が気に入らない場合は、データを小さなチャンクに格納する独自のストリームを作成します(例:List<byte[1024 * 64]>)。このようなアルゴリズムは、無駄の量を64KBに制限します。

16
usr

最後の呼び出しではなく、割り当てられたメモリの合計量を確認しているようです。メモリストリームは再割り当て時にサイズが2倍になるため、毎回約2倍に増加します。したがって、割り当てられたメモリの合計は、次のようにおよそ2の累乗の合計になります。

和 i = 1k (2)= 2k + 1 -1。

(ここで、kは、k = 1 + logのような再割り当ての数です。2 StreamSize

それはあなたが見るものについてです。

6
Alexei Levenkov

ストリームの容量を増やすということは、新しい容量でまったく新しいアレイを作成し、古いアレイをコピーすることを意味します。これは非常に高額であり、Writeごとに実行すると、パフォーマンスが大幅に低下します。したがって、代わりに、MemoryStreamは必要以上に拡張されます。その動作を改善したいが、必要な総容量がわかっている場合は、MemoryStreamコンストラクターをcapacityパラメーターとともに使用するだけです:)次にToArrayの代わりにMemoryStream.GetBufferを使用することもできます。

また、メモリプロファイラーに破棄された古いバッファーが表示されます(たとえば、8MiBから16MiBなど)。

もちろん、単一の連続した配列を持つことは気にしないので、必要に応じて作成された複数の配列を必要なだけ大きなチャンクで使用する独自のメモリストリームを用意してから、一度にすべてを出力byte[]にコピーするだけです(byte[]が必要な場合でも、おそらくそれは設計上の問題です)。

2
Luaan