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

Un comentario sobre “La memoria en .NET, tipos de variables”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *