Почему поток памяти C# резервирует так много памяти?

наше программное обеспечение распаковывает определенные байтовые данные через GZipStream, который считывает данные из MemoryStream. Эти данные распаковываются в блоки по 4КБ и записываются в другой MemoryStream.

мы поняли, что память, выделяемая процессом, намного выше, чем фактические распакованные данные.

пример: Сжатый массив байтов с 2 425 536 байтами распаковывается до 23 050 718 байт. Профилировщик памяти, который мы используем, показывает, что метод MemoryStream.set_Capacity(Int32 value) выделено 67,104,936 байт. Это коэффициент 2,9 между зарезервированной и фактически записанной памятью.

Примечание: MemoryStream.set_Capacity вызывается из MemoryStream.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 Studio 2008

4 ответов


, потому что это алгоритм для того, как он расширяет свои возможности.

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;
        }
    }
}

таким образом, каждый раз, когда вы нажмете предел емкости, он удваивает размер емкости. Причина этого в том, что 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();
    }
}

MemoryStream удваивает свой внутренний буфер, когда у него заканчивается пространство. Это может привести к 2x отходов. Я не могу сказать, почему вы видите больше, чем это. Но это основное поведение ожидается.

Если вам не нравится это поведение, напишите свой собственный поток, который хранит свои данные в меньших кусках (например, a List<byte[1024 * 64]>). Такой алгоритм ограничил бы количество отходов 64KB.


Похоже, вы смотрите на общий объем выделенной памяти, а не на последний вызов. Поскольку поток памяти удваивает свой размер при перераспределении, он будет расти примерно в два раза каждый раз - поэтому общая выделенная память будет примерно суммой мощностей 2, таких как:

Sum i=1 k (2Я) = 2k+1 -1.

(где k - количество перераспределений, таких как k = 1 + log2 StreamSize

что о том, что ты видишь.


ну, увеличение емкости потоков означает создание совершенно нового массива с новой емкостью и копирование старого. Это очень дорого, и если вы сделали это для каждого Write, ваша производительность сильно пострадает. Поэтому вместо этого MemoryStream расширяется больше, чем нужно. Если вы хотите улучшить это поведение И знаете общую требуемую емкость, просто используйте MemoryStream конструктор capacity параметр:) затем вы можете использовать MemoryStream.GetBuffer вместо ToArray тоже.

вы также видите отброшенные старые буферы в профилировщике памяти (например, от 8 MiB до 16 MiB и т. д.).

конечно, вы не заботитесь о том, чтобы иметь один последовательный массив, поэтому вам может быть лучше просто иметь собственный поток памяти, который использует несколько массивов, созданных по мере необходимости, в больших кусках, а затем просто скопировать его все сразу на выход byte[] (если вам еще нужны byte[] вообще - вполне вероятно, что это дизайн проблема.)