C++/CLI: Bloqueos y punteros interiores

Bloqueos
El lector habitual de este blog debe estar ya acostumbrado a leer aquí sobre el montículo manejado y el hecho de que las referencias en .NET son punteros móviles, es decir, apuntan y siguen al objeto en cuestión a lo largo de todos sus movimientos dentro del citado montículo, ya que uno de los elementos más potentes del .NET es la compactación de objetos asignados y la recolección de los que se han liberado, ya sea de forma automática al salir de ámbito o bien mediante su marcado manual para liberación (uso de delete en C++/CLI y de Dispose() en C#).


Pues bien, en esta entrada vamos a hablar sobre cómo bloquear un elemento dentro de dicho montículo y cómo acceder mediante un puntero a su interior.


Una referencia a un objeto no es más que una variable almacenada en la pila y que en su interior contiene los datos suficientes y necesarios para que el sistema de ejecución del .NET pueda actualizar la posición que el propio objeto ocupa dentro del montículo manejado.


Por ejemplo, la sentencia



   1: Object ^o=gcnew Object();

reserva en la pila manejada y en el contexto actual un bloque de bytes con la etiqueta “o” que a su vez contiene un puntero a otro bloque de memoria situado en el montículo manejado, y que se corresponde a la representación física de lo que quiera que sea un Object.


Cuando ejecutamos la línea



   1: o->ToString();

el compilador implementa una serie de instrucciones que miran en “o” dónde está realmente el objeto en el montículo manejado y ejecutan el método ToString() sobre dicho objeto.


De igual forma, cuando el método o función en donde se ha declarado “o” termina su ejecución, “o” deja de existir, pero no así el bloque dentro del montículo manejado. Lo que en un lenguaje como C++ sería una fuga de memoria muy seria, en C++/CLI y dentro del universo .NET no es más que un paso perfectamente válido para que, al dejar huérfano lo que venía representado por “o”, quede marcado para su futura destrucción cuando se produzca una pasada del recolector de basura.


Supongamos ahora que seguimos con nuestro objeto dentro de ámbito y que se ha producido una pasada de compactación de memoria. ¿Qué ha ocurrido? Pues muy posiblemente, que el objeto situado en el montículo haya sido desplazado a otra parte, y a su vez se haya actualizado la estructura “o” guardada en la pila para que apunte a la nueva localización.


Pero quizás nos interese bloquear la posición que el objeto ocupa en el montículo, es decir, fijar su dirección física para, por ejemplo, pasar dicha dirección a una función de código nativo, que por supuesto no entiende ni de recolectores de basura ni de movimientos no controlados de memoria ni de nada de eso. De momento, el único lenguaje que permite esto es C++/CLI mediante el uso de pin_ptr. La forma en la que otros lenguajes .NET permiten hacer algo parecido es mediante el Interop por atributos, pero desde luego no de forma tan directa.


Veámoslo con un ejemplo sencillo.



   1: #include «stdafx.h»
   2:  
   3: using namespace System;
   4:  
   5: #pragma unmanaged
   6: double nativoMedia(int *p,int tam)
   7: {
   8:     int suma=0;
   9:     for(int i=0;i<tam;i++)
  10:         suma+=p[ i];
  11:     return suma/tam;
  12: }
  13: #pragma managed
  14:  
  15: ref class Mates
  16: {
  17:     array<int>^m_numeros;
  18: public:
  19:     void Incializa(int cantidad)
  20:     {
  21:         m_numeros=gcnew array<int>(cantidad);
  22:         for(int i=0;i<cantidad;i++)
  23:             m_numeros[ i]=i;
  24:     }
  25:     property array<int>^Array
  26:     {
  27:         array<int>^get(void) {return m_numeros;}
  28:     }
  29: };
  30:  
  31: int main(array<System::String ^> ^args)
  32: {
  33:     Mates ^mates=gcnew Mates;
  34:     mates->Incializa(100);
  35:  
  36:     pin_ptr<int> p=&mates->Array[0];
  37:     int *pp=p;
  38:     Console::WriteLine(«La media {0}»,nativoMedia(pp,100));
  39:     Console::ReadKey();
  40:     return 0;
  41: }

El primer bloque de código es una función de código nativo que realiza la media de un array de enteros. Para asegurarnos de que es una función nativa, la englobamos mediante las dos directivas #pragma. Esto no es necesario cuando estamos construyendo código para producción, ya que el compilador se encargará de hacerlo de la mejor manera posible, pero para nuestro ejemplo debemos asegurarnos de que dicha función es completamente nativa. Es de observar que dentro de la función, estamos usando un puntero como un array. Si queremos hacerlo con sintaxis de punteros, una forma de código podría ser:


double nativoMedia(int *p,int tam)



   1: double nativoMedia(int *p,int tam)
   2: {
   3:     int suma=0;
   4:     for(int i=0;i<tam;i++)
   5:         suma+=*p++;
   6:     return suma/tam;
   7: }

Luego definimos una clase que contiene un array manejado de enteros y que posee un método público que se encarga de llenar el array con una serie de valores consecutivos. Lo siguiente es una propiedad que nos devuelve una referencia al propio array interno, lo que en un código de producción no es una buena idea (la de exponer las interioridades de una clase), pero para nuestro ejemplo nos sirve perfectamente.


Ya en main() creamos un objeto del tipo Mates y lo inicializamos con los 100 primeros números. Y entonces viene lo interesante. La línea



   1: pin_ptr<int> p=&mates->Array[0];

está declarando un elemento p que es un puntero bloqueado al primer valor del array interno de la clase Mates. Lo que internamente hace es marcar el array m_numeros como bloqueado e inamovible, y luego devuelve la dirección real del primer elemento dentro del montículo manejado. Esto requiere la explicación de que los arrays .NET están almacenados de forma contigua dentro del montículo manejado, es decir, cuando nosotros creamos un array manejado, el sistema reserva un bloque de memoria manejada capaz de contener todo el array de una sola vez (de ahí que si necesitamos cambiar su tamaño, debemos crear un nuevo array con el nuevo tamaño y copiar el antiguo sobre el nuevo). Esto es así por definición.


Pero un pin_ptr<int> no es un puntero nativo a un entero, sino que se trata de un tipo nuevo que almacena una dirección que apunta a algo bloqueado dentro del montículo manejado. Para convertirlo a un int* debemos asignarlo a una variable de este tipo, que es lo que hacemos en la siguiente línea. El resto es trivial.


Pero si esto fuera código de producción habríamos cometido un error bastante serio. ¿Se da alguien cuenta de cuál es? Que el lector piense antes de seguir leyendo.


El error consiste en que hemos dejado bloqueado un elemento dentro del montículo manejado y sólo se libera a la hora de cerrar el programa, que en nuestro caso da igual, pero en otras situaciones podría tener serias consecuencias de rendimiento y podría incluso llegar a impedir la asignación de nuevos bloques de memoria dentro del montículo manejado. El motivo es muy sencillo: el compactador, asignador y liberador de memoria del montículo manejado ha de tener completa libertad a la hora de poder mover todo lo que haya en él. El autor ignora si posee algún tipo de optimización para lidiar con elementos bloqueados (imagina que sí), pero desde luego no será tan potente como el asignador nativo (que se usa cuando hacemos new o malloc), que es capaz de buscar el bloque más óptimo de entre todos los posibles –de ahí que la asignación de memoria en .NET sea algo más rápida que su equivalente nativo en determinadas circunstancias.


La solución a este problema pasa por hacer algo como lo expuesto más abajo o por mantener dicho puntero bloqueado el menor tiempo posible.



   1: int main(array<System::String ^> ^args)
   2: {
   3:     Mates ^mates=gcnew Mates;
   4:     mates->Incializa(100);
   5:     {
   6:         pin_ptr<int> p=&mates->Array[0];
   7:         int *pp=p;
   8:         Console::WriteLine(«La media es {0}»,nativoMedia(pp,100));
   9:     }
  10:     Console::ReadKey();
  11:     return 0;
  12: }

NOTA: Al rodear un bloque de código mediante llaves dentro de una función, forzamos al compilador a que cree un nuevo contexto local, siendo equivalente a una llamada a función, de modo que p y pp son variables locales dentro de este bloque, y quedan liberadas en la llave de cierre, de forma que el bloqueo de nuestra variable apenas ha durado lo necesario.


Punteros interiores
¿Se puede optimizar el código de arriba de alguna forma? ¿Podemos evitar bloquear un objeto? La respuesta directa es que la situación comentada consiste en la mejor solución posible, pero dependiendo de circunstancias, podemos evitarnos el bloqueo de todo un objeto mediante el uso de punteros interiores, aunque esta vez no podamos salirnos del código manejado.


Retomemos el código anterior y modifiquémoslo un poco:



   1: #include «stdafx.h»
   2:  
   3: using namespace System;
   4:  
   5: ref class Mates
   6: {
   7:     array<int>^m_numeros;
   8: public:
   9:     void Incializa(int cantidad)
  10:     {
  11:         m_numeros=gcnew array<int>(cantidad);
  12:         for(int i=0;i<cantidad;i++)
  13:             m_numeros[ i]=i;
  14:     }
  15:     property array<int>^Array
  16:     {
  17:         array<int>^get(void) {return m_numeros;}
  18:     }
  19: };
  20:  
  21: int main(array<System::String ^> ^args)
  22: {
  23:     Mates ^mates=gcnew Mates;
  24:     mates->Incializa(10);
  25:     for(int i=0;i<10;i++) Console::Write(«{0} «,mates->Array[ i]);
  26:     Console::WriteLine(«»);
  27:  
  28:     interior_ptr<int>medio=&mates->Array[5];
  29:     *medio=99;
  30:     for(int i=0;i<10;i++) Console::Write(«{0} «,mates->Array[ i]);
  31:  
  32:     Console::ReadKey();
  33:     return 0;
  34: }

La clase Mates es la misma, pero el interior de main() ha cambiado por completo. El código que nos interesa está en las líneas



   1: interior_ptr<int>medio=&mates->Array[5];
   2: *medio=99;

que nos muestra cómo obtener un puntero interior. Aunque el ejemplo no demuestra ninguna utilidad, podemos observar cómo se hace, pero tiene una serie de ventajas que pasamos a explicar.


Un puntero interior permite semántica de punteros para objetos de tipo .NET siempre y cuando no nos salgamos del interior de un objeto (de ahí su nombre). Y tienen la ventaja de que son objetos manejados –es decir, siguen siendo móviles y no afectan para nada al recolector ni al asignador-.


Un puntero interior se puede aplicar a cualquier elemento situado dentro del montículo manejado, aunque tiene ciertas limitaciones: no puede ser miembro de una clase, sólo se puede declarar en la pila y no se puede obtener su dirección, es decir, no podemos obtener un puntero interior de un puntero interior, y tampoco se puede pasar como puntero a un bloque de código nativo.


¿Entonces, para qué me sirve? Buena pregunta. Sigamos.


Podemos obtener un puntero interior a un elemento de un array, bloquearlo conforme hemos visto en la sección anterior a través de un pin_ptr y pasarlo a un bloque de código nativo. Como en este caso estamos bloqueando una parte de un objeto secuencial, realmente estamos bloqueando el objeto completo, por lo que poco o nada hemos ganado respecto a lo de antes. ¿Entonces qué?


En primer lugar, si dicho objeto fuera un elemento agregado que contuviera referencias a otros elementos del montículo, al obtener un puntero interior a un elemento agregado y fijándolo después, sólo bloquearíamos el objeto secundario, no el primario, mejorando y optimizando en cierta medida la cantidad de memoria no móvil.


Y en segundo lugar, imaginemos que tenemos una biblioteca o un código fuente que funciona a las mil maravillas con C++ y que trabaja, para desgracia nuestra, con punteros normales. Si disponemos del código fuente, podemos adaptar su funcionamiento sólo cambiando la firma de los métodos sin necesidad de tocar nada del cuerpo de los mismos. Tomando el ejemplo anterior, sólo tenemos que cambiar la firma del método



   1: double nativoMedia(int *p,int tam)

por la de



   1: double nativoMedia(interior_ptr<int> p,int tam)

y dejar el cuerpo del mismo sin tocar. De este modo nos evitamos modificar lo que realmente resulta peligroso dentro del código fuente: los algoritmos y las implementaciones.


Ya para terminar, pongamos el ejemplo completo:



   1: #include «stdafx.h»
   2:  
   3: using namespace System;
   4:  
   5: double nativoMedia(interior_ptr<int> p,int tam)
   6: {
   7:     int suma=0;
   8:     for(int i=0;i<tam;i++)
   9:         suma+=*p++;
  10:     return suma/tam;
  11: }
  12: ref class Mates
  13: {
  14:     array<int>^m_numeros;
  15: public:
  16:     void Incializa(int cantidad)
  17:     {
  18:         m_numeros=gcnew array<int>(cantidad);
  19:         for(int i=0;i<cantidad;i++)
  20:             m_numeros[ i]=i;
  21:     }
  22:     property array<int>^Array
  23:     {
  24:         array<int>^get(void) {return m_numeros;}
  25:     }
  26:  
  27: };
  28:  
  29: int main(array<System::String ^> ^args)
  30: {
  31:     Mates ^mates=gcnew Mates;
  32:     mates->Incializa(100);
  33:  
  34:     interior_ptr<int>p=&mates->Array[0];
  35:     Console::WriteLine(nativoMedia(p,100));
  36:  
  37:     Console::ReadKey();
  38:     return 0;
  39: }

6 comentarios sobre “C++/CLI: Bloqueos y punteros interiores”

  1. Excelente artículo, Rafa. Sólo una pega achacable al grñfbmtnxs del Community Server: tiene la fea costumbre de cambiar los «[ i ]» por bombillas tipo «¡tengo una idea genial!».

  2. ¡Excelente! Me ha recordado el cuadernillo de Sutter que tradujimos, con el que aprendí bastante. Por cierto, deberías colgarlo aquí, seguro que alguien lo pilla…

    Slds – Octavio

  3. Pozí. O incluso publicarlo por capítulos en una nueva sección…

    De todos modos, en varias etapas de la redacción de esto me ha venido a la cabeza el mismo.

    Desde luego que valió la pena el tiempo y el esfuerzo empleado en traducirlo, que fue mucho.

  4. ¿Podrías explicar esto de nuevo?

    «-de ahí que la asignación de memoria en .NET sea algo más rápida que su equivalente nativo en determinadas circunstancias.»

    Es que dudo entre si yo no he entendido bien el párrafo entero o has dicho una soberana estupidez.

  5. mmm, es correcto, pues ocurre que a veces, dado el modelo de asignación de memoria de .NET frente al del runtime de C/C++, el .NET es capaz de asignar memoria *mucho* más rápido que el C++.

    Ojo, una cosa es hacer LocalAlloc() y otra muy diferente, usar new o malloc.

    En el segundo caso, existe un gestor de memoria aparte de Win32 que va recorriendo una lista enlazada para encontrar el hueco correcto, sin embargo en .NET, cuando se pide un bloque, éste se da inmediatamente ya que en general la memoria está compactada y siempre se atina al primer intento.

    Y en C++ todavía es peor: conforme el programa se va ejecutando y va asignando/liberando bloques de memoria, la asignación se va volviendo cada vez más lenta hasta que, en determinadas situaciones, se pueden tener varios megas de RAM ocupados con apenas unos KB reales; esto lo soluciona la memoria virtual de Win32, pero depende de cómo se asignen, hay veces que no puede.

Responder a rfog Cancelar respuesta

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