Alto consumo de memoria con Enumerable.Range?

Originalmente quería saber si ToList asigna más memoria que usar el constructor de List que toma un IEnumerable (sin diferencia).

Para fines de prueba, utilicé Enumerable.Range para crear una matriz fuente que podría usar para crear una instancia de List través de 1. ToList y 2. constructor . Ambos están creando copias.

Así es como me di cuenta de una gran diferencia en el consumo de memoria entre:

  1. Enumerable.Range(1, 10000000) o
  2. Enumerable.Range(1, 10000000).ToArray()

Cuando uso el primero y llamo a ToList el objeto resultante pierde aproximadamente un 60% más de memoria que la Matriz (38,26MB / 64MB).

P: ¿Cuál es la razón de esto o dónde está mi error de razonamiento?

 var memoryBefore = GC.GetTotalMemory(true); var range = Enumerable.Range(1, 10000000); var rangeMem = GC.GetTotalMemory(true) - memoryBefore; // negligible var list = range.ToList(); var memoryList = GC.GetTotalMemory(true) - memoryBefore - rangeMem; String memInfoEnumerable = String.Format("Memory before: {0:N2} MB List: {1:N2} MB" , (memoryBefore / 1024f) / 1024f , (memoryList / 1024f) / 1024f); // "Memory before: 0,11 MB List: 64,00 MB" memoryBefore = GC.GetTotalMemory(true); var array = Enumerable.Range(1, 10000000).ToArray(); var memoryArray = GC.GetTotalMemory(true) - memoryBefore; list = array.ToList(); memoryList = GC.GetTotalMemory(true) - memoryArray; String memInfoArray = String.Format("Memory before: {0:N2} MB Array: {1:N2} MB List: {2:N2} MB" , (memoryBefore / 1024f) / 1024f , (memoryArray / 1024f) / 1024f , (memoryList / 1024f) / 1024f); // "Memory before: 64,11 MB Array: 38,15 MB List: 38,26 MB" 

Esto probablemente se relaciona con el algoritmo de duplicación utilizado para cambiar el tamaño del búfer de respaldo cuando se agrega a una lista. Cuando se asigna como una matriz, se conoce su longitud y se puede consultar comprobando IList[] y / o ICollection[] ; por lo tanto, puede asignar una única matriz, la del tamaño correcto la primera vez, y luego solo bloquear el contenido.

Con la secuencia esto no es posible (la secuencia no expone la longitud de ninguna manera accesible); por lo tanto, debe volver a “seguir llenando el búfer, si está lleno, doblarlo y copiarlo”.

Obviamente, esto necesita aproximadamente el doble de memoria.

Una prueba interesante sería:

 var list = new List(10000000); list.AddRange(Enumerable.Range(1, 10000000)); 

Esto asignará el tamaño correcto inicialmente, mientras sigue usando la secuencia.

tl; dr; el constructor, al pasar una secuencia, primero verifica si puede obtener la longitud mediante conversión a una interfaz conocida.

Se debe al algoritmo de duplicación utilizado para crear la matriz de respaldo en una lista. IEnumerable no tiene propiedad Count, por lo que no puede preasignar la matriz de respaldo para que sea el tamaño objective al llamar a ToList. De hecho, en cada llamada a MoveNext está llamando al complemento correspondiente en la lista.

Sin embargo, Array.ToList puede anular el comportamiento ToList base para inicializar la lista a la capacidad correcta. Además, podría ser la Lista en su constructor que intente disminuir la referencia de IEnumerable que tiene a tipos de colección conocidos como IList, ICollection, Array, etc.

Actualizar

De hecho, es en el constructor de List que se da cuenta si el argumento implementa ICollection:

 public List(IEnumerable collection) { if (collection == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); ICollection collection1 = collection as ICollection; if (collection1 != null) { int count = collection1.Count; if (count == 0) { this._items = List._emptyArray; } else { this._items = new T[count]; collection1.CopyTo(this._items, 0); this._size = count; } } else { this._size = 0; this._items = List._emptyArray; foreach (T obj in collection) this.Add(obj); } } 

La lista se implementa como una matriz. Cuando excede lo que ha asignado, asigna otra matriz al doble del tamaño (esencialmente duplicando la asignación de memoria). La capacidad predeterminada es 4 y duplica las cosas a partir de ahora.

Lo más probable es que si suelta la cantidad de elementos para decir 7.500, verá que la matriz baja a un poco menos de 32 MB, y el tamaño IList es de 32 MB.

Puede decirle a IList cuál debe ser el tamaño inicial, por lo que si le da IEnumerable en el momento de la construcción, no debería sobre asignar la memoria.

[Editar] después de comentarios

En el caso de Enumerable.Range(a, b) , devuelve un IEnumerable solamente en lugar de un ICollection . Para List no sobreasignar, el elemento pasado durante la construcción también debe ser una ICollection

Supongo:

  • Enumerable.Range(1, 10000000) solo crea un IEnumerable y no crea elementos todavía.

  • Enumerable.Range(1, 10000000).ToArray() crea una matriz, utilizando la memoria para los números

  • Enumerable.Range(1, 10000000).ToList() crea los números y datos adicionales para administrar la lista (enlaces entre partes. La lista puede cambiar su tamaño y necesita asignar memoria en bloques).