La memoria en .NET, tipos de variables

Siguiendo con el el artículo anterior de la memoria en .NET donde explicaba como esta estructurada, sigo con las variables, que en .NET principalmente son de dos tipos:

  • Tipos por valor:
Tipo (alias) Bytes Rango
Char (char) 2 Caracteres
Bolean (bool) 4 True ó False
IntPtr ? Puntero nativo
DateTime (date) 8 1/1/0001 12:00:00 AM a 12/31/9999 11:59:59 PM
SByte (sbyte) 1 -128 a 127
Byte (byte) 1 0 a 255
Int16 (short) 2 -32768 a 32767
Int32 (int) 4 -2147483648 a 2147483647
UInt32 (uint) 4 0 a 4294967295
Int64 (long) 8 -9223372036854775808 a 9223372036854775807
Single (float) 4 -3.402823E+38 a 3.402823E+38
Double (double) 8 -1.79769313486232E+308 a 1.79769313486232E+308
Decimal (decimal) 16 -79228162514264337593543950335 a 79228162514264337593543950335
    • Se conoce su tamaño antes de su inicialización, el más grande tiene 16 Bytes.
    • Heredan de System.ValueType y ninguno puede ser extendido (sealed)(ni siquiera ValueType).
    • Se almacenan en el thread stack cuando son variables locales y expresiones intermedias, y en el managed heap en el resto de situaciones (variables globales ó/y estáticas).
    • Las estructuras (struct) y enumeraciones (enum) son tipos por valor también y responden al mismo comportamiento.
    • La memoria ocupada por estos tipos se elimina en cuanto están fuera de ámbito en caso de estar en el thread stack, y por medio del GC cuando estan en el managed heap (desapareciendo junto con la clase a la que está asociada). 

 

  • Tipos por referencia (objeto):
    • No se conoce su tamaño hasta después de su inicialización.
    • Todos los demás tipos de la BCL.
    • Heredan directamente de System.Object, y pueden ser extendidos a no ser que se especifique lo contrario (sealed).
    • Se almacenan siempre en el managed heap, y son accedidos desde el thread stack mediante una referencia.
    • Cuando declaramos una variable de tipo objeto, se crea una referencia en el thread stack, y al crear la instancia en el managed heap por medio del operador new, su dirección en memoria se asigna a la susodicha variable. De esta forma se puede acceder al objeto por medio de la indirección de la referencia.
    • La memoria ocupada por estos tipos la libera el GarbageCollector de forma no determinística.

Si, no me he equivocado, los tipos por valor no se almacenan siempre en el thread stack :D, como decía solo se almacenan ahí cuando son variables locales y expresiones intermedias.

Como decía en el artículo anterior, el thread stack es un espacio de almacenamiento exclusivo de su hilo de ejecución y no puede ser compartido, sin embargo… te pones a pensar y te das cuenta de que puedes compartir variables de tipo por valor entre threads… por lo que lo primero que te viene a la mente es que este realizando boxing/unboxing para ello… pero si precisamente una de las metas del performance es evitar esas dos operaciones … no tiene sentido!

Menos mal que tiene uno el .NET Reflector siempre a mano para salir de la penumbra xD

Vamos a ver como se comporta el CLR con una variable local y una variable externa de otra clase, ambas de tipo por valor:

class Program
{
  static void Main(string[] args)
  {
    MyClass exampleClass = new MyClass();
    exampleClass.globalInt = 6;
 
    Int32 localInt = 5;
 
    Console.WriteLine(localInt);
    Console.WriteLine(exampleClass.globalInt);
  }
}
 
class MyClass
{
  public Int32 globalInt;
}

localInt es un Int32 declarado localmente y debería estar en el thread stack, y MyClass.globalInt es un Int32 declarado como miembro de un tipo por referencia, por lo que debería estar en el managed heap… vamos a verlo. El método Main genera el siguiente código CIL (obviadas las instrucciones que no interesan ahora):

   1:      .locals init (
   2:          [0] class Test.MyClass exampleClass,
   3:          [1] int32 localInt)
   4:      L_0000: newobj instance void Test.MyClass::.ctor()
   5:      L_0005: stloc.0 
   6:      L_0006: ldloc.0 
   7:      L_0007: ldc.i4.6 
   8:      L_0008: stfld int32 Test.MyClass::globalInt
   9:      L_000d: ldc.i4.5 
  10:      L_000e: stloc.1 
  11:      L_000f: ldloc.1 
  12:      L_0010: call void [mscorlib]System.Console::WriteLine(int32)
  13:      L_0015: ldloc.0 
  14:      L_0016: ldfld int32 Test.MyClass::globalInt
  15:      L_001b: call void [mscorlib]System.Console::WriteLine(int32)
  16:      L_0020: ret 

Lo primero que uno mira es si aparecen las instruciones box/unbox para realizar boxing/unboxing, pero como vemos no aparecen, por lo cual, se esta accediendo y asignando a una variable de tipo por valor en el heap sin realizar ninguna de estas dos operaciones.

Voy a explicarlo brevemente:

  • Asignación del miembro de MyClass, líneas 7 y 8: carga en valor ‘6’ en pila y lo asigna mediante la instrucción stfld, que precisamente sirve para asignar valores a una variable contenida en un objeto.
  • Asignación de la variable local localInt, líneas 9 y 10: carga el valor ‘5’ en pila y lo asigna mediante la instrución stloc a la variable local en el puesto 1 (la 0 es Test.MyClass como se puede ver en las 3 primeras líneas).

Queda claro que el tratamiento es distinto y que stfld sirve para manipular tipos por valor asociados a una instancia en el managed heap, de hecho si lo piensas bien, una vez que has localizado la instancia en el managed heap por medio de la referencia … ¿porque hacer boxing/unboxing con ella? … 😛

Al igual pasa si lo hacemos con un campo estático, hay una instrucción especial para ello:

class Program
{
  static Int32 intTest;
 
  static void Main(string[] args)
  {
    intTest = 6;
    Console.WriteLine(intTest);
  }
}

CIL:

   1:      L_0000: ldc.i4.6 
   2:      L_0001: stsfld int32 Test.Program::intTest
   3:      L_0006: ldsfld int32 Test.Program::intTest
   4:      L_000b: call void [mscorlib]System.Console::WriteLine(int32)
   5:      L_0010: ret 

Como podemos ver utiliza otra instrución distinta, stsfld, para tratar con variables estáticas.

.NET Reflector da una orientación de que significa cada instrucción.

Así que vuelvo a repetir, los tipos por valor no se almacenan siempre en el thread stack como se viene diciendo en muchos sitios, solo se almacenan ahí cuando son variables locales y expresiones intermedias. El CIL tiene sus propias instruciones para acceder a tipos por valor cuando se encuentran en el managed heap, además del boxing/unboxing para almacenarlos allí directamente.

Próximo capítulo… boxing/unboxing a fondo.

 

Crossposting from vtortola.NET

La memoria en .NET, Managed heap y Thread stack

Con este, quiero empezar una serie de pequeños artículos sobre como esta estructurada la memoria en .NET, sin profundizar en demasiados detalles… pero empezar por lo más simple y acabar hablando sobre temas de sincronización, atomicidad y volaticidad pasando por el GarbageCollector, intentando aportar una información que a mi parecer, todo desarrollador debería tener en mente a la hora de programar… y que se debe conocer en profundidad para desarrollar aplicaciones multithreading. Espero que sea útil para quien lo lea y personalmente me ayude a comprenderlo mejor.

La memoria de la que hacen uso nuestras aplicaciones administradas se divide en dos partes principalmente:

  • Thread stack:
    • Espacio de memoria asociado al hilo de ejecución.
    • Es la pila donde va progresando nuestro código.
    • Aquí se van “apilando” las llamadas a funciones y las variables locales y parámetros, de forma que el puntero de ejecución va cargando, ejecutando y liberando métodos con sus respectivas variables.
    • No se pueden compartir ese espacio entre varios hilos, esta únicamente ligado un hilo de ejecución.
    • Como decía es el espacio de memoria asociado al hilo de ejecución, por lo que es bastante rápido. 
    • Está limitado a un máximo de 1MByte por hilo.
    • Si se intenta superar el límite de memoria se obtiene un StackOverflowException.
    • Aquí se almacenan tipos de los cuales se conoce su tamaño antes de su inicialización (tipos por valor) y referencias a objetos del managed heap, que contienen la dirección de memoria del objeto allí ó un nulo si no referencian nada.
    • El proceso de liberar memoria de este espacio se realiza de forma determinística por el puntero de ejecución.

  • Managed Heap:
    • Espacio de memoria asociado al proceso.
    • Compartido entre los hilos que lo forman (si hubiese varios).
    • Accedido mediante punteros (dirección/indirección) lo que lo hace más lento que el thread stack, pero permite ser accedido desde otros hilos.
    • No tiene una limitación en su tamaño que no sea la del hardware.
    • Normalmente almacena tipos por referencia y tipos por valor cuando tienen una relación de composición con un tipo por referencia.
    • Si se superar el máximo de memoria disponible se obtiene un OutOfMemoryException.
    • El proceso de liberar memoria de este espacio se realiza de forma no determinística por el recolector de basura (GarbageCollector).

 Como se puede ver, nuestra ejecución reside en el thread stack (ya que MSIL es stack based), el puntero de ejecución va cargando lo que necesita para la ejecución y liberandolo en cuanto acaba. El problema viene cuando utilizamos tipos de los que no podemos saber la memoria que ocupan hasta después de su inicialización, entonces dependemos de otra zona de memoria “ilimitada” y de acceso mediante punteros llamada managed heap donde instanciamos dicho tipo, de forma que nuestro puntero de ejecución tiene una referencia en el thread stack apuntando a esa instancia pero no la instancia en si, lo que hace más lento el acceso al tener que recurrir a la indirección de la referencia.

Cuando hablamos de una referencia, estamos hablando de un puntero seguro y tipado, de forma que no puede apuntar a una dirección de memoria cualquiera, solo a la ubicación en memoria de una instancia de un tipo dado ó ser nulo(null).

El managed heap, al contrario del thread stack, puede ser compartido por varios hilos, lo cual no significa que pueda ser libremente compartido. Siempre que se vaya a trabajar con una instancia suceptible de ser accedida por múltiples hilos… es necesario sincronizar el acceso para evitar condiciones de anticipación y/ó dejarla en un estado inconsistente. Además, la memoria de este espacio no puede ser liberada de forma determinista, debe delegarse en un proceso en segundo plano llamado recolector de basura (GarbageCollector) encargado de liberar la memoria usada por instancias que no son referenciadas desde ningún thread stack.

En el próximo capítulo… las variables, tipos por valor y referencia. 

Crossposting desde vtortola.NET

Primer post en Geeks.ms

geeksms  Por invitación de Rodrigo Corral a partir de ahora también postearé en Geeks.ms, comunidad que sigo desde hace tiempo y que es todo un privilegio para mi colaborar en ella.

Me llamo Valeriano, actualmente vivo en Madrid, trabajo en Avanade y llevo unos dos años y medio dándole a esto del .NET con C#. Tengo especial curiosidad por la programación concurrente que espero poco a poco ir dominando, por el .NET framework y C# en general. Espero compartir con vosotros todo lo que vaya aprendiendo sobre estos temas 😀

Un saludo.