El foreach puede causar problemas de memoria

Después de tener algo de tiempo he ido a mi lista de cosas por leer y me quedado a cuadros cuando lo he leído.

http://blogs.msdn.com/b/etayrien/archive/2007/03/17/foreach-garbage-and-the-clr-profiler.aspx

Sí es un enlace del 2007, lo sé, imaginaos la de cosas que tengo en esa lista :P.

En resumen, imaginemos este código:


class Program
{
    class GameEntity
    {
        public void Update()
        {
        }
    }

    static GameEntity[] entities = new GameEntity[100];
    static Program()
    {
        for (int i = 0; i < entities.Length; i++)
        {
            entities[i] = new GameEntity();
        }
    }

    static void Main(string[] args)
    {
        byte[] byteArray = new byte[1];
        for (int i = 0; i < entities.Length; i++)
        {
            entities[i].Update();
        }
    }
}

En el post original, después de pasar el CLR Profiler no se hace reserva de memoria para un enumerador para poder recorrer la colección, algo lógico. Después, para ver la diferencia con el foreach sustituye el código del for por este otro:

static void Main(string[] args)
{
   byte[] byteArray = new byte[1];
   foreach (GameEntity e in entities)
   {
      e.Update();
   }
}

En el caso del foreach se reserva memoria para un enumerador necesario para recorrer el foreach.

Y todo va perfecto, sin embargo, hay un escenario en el que se pueden producir fugas de memoria:

const int NumEntities = 100;

static List list = new List();
static Program()
{
   for (int i = 0; i < NumEntities; i++)
   {
         list.Add(new GameEntity());
     }
}
static void Main(string[] args)
 {
     UpdateIEnumerable(list);
 }
private static void UpdateIEnumerable(IEnumerable enumerable)
 {
     foreach (GameEntity e in enumerable)
     {
         e.Update();
     }
 }

En este caso sí se producen fugas de memoria. Y es que aunque estemos haciendo un foreach en una lista, cuando se le hace un casting a una interfaz, al tipo valor del enumerador se le hace un box, y se coloca en el heap.

La conclusión:

  • Cuando hacemos un foreach sobre un Collection<T> se reserva memoria para un enumerador.
  • Cuando hacemos un foreach sobre la mayoría de las colecciones, como arrays, listas, colas, listas enlazadas y otras:
    • Si se usan explícitamente, NO se reserva memoria para un enumerador.
    • Si se usan a través de interfaces, se reserva memoria para un enumerador.

Así que si el consumo de memoria es algo crítico en vuestra aplicación o juego , nunca, nunca, uséis interfaces para recorrer una colección.

 

[Update: Gracias a Bernardo por el comentario]

El problema aparece cuando el “foreach” recorre la colección como si fuera un “IEnumerable”. En este caso se utiliza la implementación explícita de “GetEnumerator()” y se realiza “boxing” del “struct” para devolver un tipo “IEnumerator”.

El “boxing” es una operación costosa y puede llegar a consumir mucha memoria.

P.D: el método “GetEnumerator()” de  “Collection” no devuelve un “struct”. Es de las pocas colecciones que son una excepción.

Espero que os haya gustado tanto como a mi. 🙂

Juan María Laó Ramos.

3 pensamientos en “El foreach puede causar problemas de memoria

  1. Bernardo

    Lo que comentas es muy interesante y también importante, sin embargo te has dejado lo fundamental: la explicación de porqué el “foreach” a veces provoca un consumo excesivo de memoria.

    La clave está en la implementación del método “GetEnumerator()” por parte de las diferentes colecciones (List, Collection, etc). Lo normal es que “GetEnumerator()” devuelva… ¡un “struct”!, por lo tanto al recorrer la colección mediante un “foreach” no se generan objetos en el “heap”.

    Quien no se lo pueda creer sólo tiene que comprobarlo decompilando el código mediante Reflection o dotPeek:

    public List.Enumerator GetEnumerator()
    {
    return new List.Enumerator(this);
    }

    public struct Enumerator : IEnumerator, IDisposable, IEnumerator
    {
    private List list;
    private int index;
    private int version;
    private T current;

    // …
    }

    El problema aparece cuando el “foreach” recorre la colección como si fuera un “IEnumerable”. En este caso se utiliza la implementación explícita de “GetEnumerator()” y se realiza “boxing” del “struct” para devolver un tipo “IEnumerator”.

    IEnumerator IEnumerable.GetEnumerator()
    {
    return (IEnumerator) new List.Enumerator(this);
    }

    El “boxing” es una operación costosa y puede llegar a consumir mucha memoria.

    P.D: el método “GetEnumerator()” de “Collection” no devuelve un “struct”. Es de las pocas colecciones que son una excepción.

      1. Bernardo

        Muchas gracias a ti. También tenía esa misma entrada en mi lista de pendientes y tu comentario me ha animado a ponerme con ello 😀

Los comentarios están cerrados.