c# memory memory-management memorystream gzipstream

¿Por qué la secuencia de memoria C#reserva tanta memoria?



memory memory-management (4)

Nuestro software descomprime ciertos datos de bytes a través de un GZipStream , que lee datos de un MemoryStream . Estos datos se descomprimen en bloques de 4 KB y se escriben en otro MemoryStream .

Nos hemos dado cuenta de que la memoria que el proceso asigna es mucho más alta que la información descomprimida real.

Ejemplo: una matriz de bytes comprimidos con 2.425.536 bytes se descomprime en 23.050.718 bytes. El generador de perfiles de memoria que usamos muestra que el Método MemoryStream.set_Capacity(Int32 value) asignó 67,104,936 bytes. Eso es un factor de 2.9 entre la memoria reservada y la realmente escrita.

Nota: MemoryStream.set_Capacity se llama desde MemoryStream.EnsureCapacity que se llama desde MemoryStream.Write en nuestra función.

¿Por qué MemoryStream reserva tanta capacidad, a pesar de que solo agrega bloques de 4 KB?

Aquí está el fragmento de código que descomprime datos:

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

Nota: si es relevante, esta es la configuración del sistema:

  • Windows XP 32bit,
  • .NET 3.5
  • Compilado con Visual Studio 2008

Bueno, aumentar la capacidad de las secuencias significa crear una nueva matriz con la nueva capacidad y copiar la anterior. Eso es muy caro, y si lo hicieras por cada Write , tu rendimiento sufriría mucho. Entonces, en cambio, MemoryStream expande más de lo necesario. Si desea mejorar ese comportamiento y conoce la capacidad total requerida, simplemente use el constructor MemoryStream con el parámetro de capacity :) Luego puede usar MemoryStream.GetBuffer lugar de ToArray también.

También está viendo los búferes antiguos descartados en el generador de perfiles de memoria (por ejemplo, de 8 MiB a 16 MiB, etc.).

Por supuesto, no te importa tener una sola matriz consecutiva, por lo que podría ser una mejor idea que simplemente tengas una secuencia de memoria propia que use varias matrices creadas según sea necesario, en trozos tan grandes como sea necesario, y luego simplemente copie todo de una vez en el byte[] salida byte[] (si incluso necesita el byte[] en absoluto, muy probablemente sea un problema de diseño).


Parece que está mirando la cantidad total de memoria asignada, no la última llamada. Como el flujo de memoria duplica su tamaño en la reasignación, crecerá aproximadamente dos veces cada vez, por lo que la memoria asignada total sería aproximadamente la suma de potencias de 2 como:

Suma i = 1 k (2 i ) = 2 k + 1 -1.

(donde k es el número de reasignaciones como k = 1 + log 2 StreamSize

Que es lo que ves


Porque este es el algoritmo de cómo expande su capacidad.

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

Entonces, cada vez que alcanzas el límite de capacidad, duplica el tamaño de la capacidad. La razón por la que hace esto es porque la operación Buffer.InternalBlockCopy es lenta para las matrices grandes, por lo que si tuviera que cambiar el tamaño con frecuencia de cada llamada de escritura, el rendimiento disminuiría significativamente.

Algunas cosas que podría hacer para mejorar el rendimiento para usted es que podría establecer la capacidad inicial para que sea al menos del tamaño de su matriz comprimida y, luego, podría aumentar el tamaño en un factor inferior a 2.0 para reducir la cantidad de memoria que está utilizando .

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

Si quisieras, podrías hacer algoritmos aún más sofisticados, como cambiar el tamaño en función de la relación de compresión actual

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 duplica su memoria interna cuando se queda sin espacio. Esto puede llevar a 2x desperdicio. No puedo decir por qué estás viendo más que eso. Pero este comportamiento básico es esperado.

Si no le gusta este comportamiento, escriba su propia transmisión que almacene sus datos en fragmentos más pequeños (por ejemplo, una List<byte[1024 * 64]> ). Tal algoritmo limitaría su cantidad de desechos a 64 KB.