El montón por defecto
Aunque tengamos varios montones adicionales, el más importante es el montón por defecto. Esto es porque, internamente, cualquier reserva de memoria utilizando funciones del lenguaje como malloc, GetMem, etc., se hace en el montón por defecto.
Siendo prácticos podemos decir que el siguiente bloque de código:
var
buffer: Pointer;
begin
GetMem(buffer, 1024);
// aquí se hace lo que sea
FreeMem(buffer);
end;
Es equivalente a este otro código:
var
buffer: Pointer;
begin
buffer := HeapAlloc(GetProcessHeap, 0, 1024);
// aquí se hace lo que sea
HeapFree(GetProcessHeap, 0, buffer);
end;
Además, cualquier reserva que haga el sistema para un proceso, también la hace en el montón por defecto. Por ejemplo, cuando hacemos una llamada a la función FindFirstFile, el sistema creará una pequeña zona de memoria para su uso interno, en el montón por defecto del proceso llamante. Esa zona de memoria se liberará con otra función, en nuestro caso con FindClose.
Debido a esto, es muy importante que los accesos al montón por defecto se hagan sincronizados, sin utilizar la bandera HEAP_NO_SERIALIZE, ya que existirán múltiples hilos que accedan a él.
Este montón, tiene un tamaño por defecto de 1 MB, de los cuales tan sólo de comprometen 4 KB.
En Delphi no es posible cambiar el tamaño del montón por defecto, aunque sí en C++ Builder, desde la opción de menú Project - Options - Linker - PE File Options. En esta sección vemos, tanto la configuración del almacenamiento de la pila, como del montón por defecto. Los campos MinHeapSize y MaxHeapSize indican el espacio a reservar y comprometer respectivamente.
Cuando explicamos el parámetro dwTamañoMaximo de la función HeapCreate, dijimos que si contenía un valor mayor que 0, el montón no podría crecer automaticamente. Esto se falso para el montón por defecto, ya que éste sí podrá crecer aunque se haya definido un tamaño máximo. De hecho, el montón por defecto crece en segmentos de 1 MB cada vez que necesita memoria.
La gente de Microsoft recomienda que el tamaño máximo sea lo suficientemente grande como para que no se llegue a una situación en que el montón por defecto deba crecer. Para ello debemos definir un tamaño máximo en el que quepan todos los objetos que vamos a crear dinámicamente durante la ejecución.
El tamaño mínimo nos indica cuánto será comprometido al crear el montón, por lo que si indicamos un tamaño muy grande, la carga de la aplicación se ralentizará (pero durante la ejecución, las reservas serán más rápidas).
Supongamos que nuestra aplicación va a manejar una lista enlazada (o un objeto TList). Cada elemento de esta lista será un puntero de 32 bits, que apuntará a una estructura creada dinámicamente. Cada una de las estructuras ocupará 234 bytes, que aplicando el alineado de campos pasará a ocupar 256 bytes. Si hacemos una estimación, y decimos que en el caso más desfavorable tendremos en memoria 10.000 estructuras, aplicamos la siguiente ecuación:
dwTamañoMaximo = (256 + 32) * 10.000 ) = 2.880.000 bytes = 2812,5 KB
En nuestro ejemplo, y para curarnos en salud, yo definiría un tamaño máximo para el montón de 3 MB.
Para calcular el tamaño mínimo, podemos hacer una estimación de la ocupación que se hará del montón durante el arranque (los objetos que van a estar en memoria durante toda la vida de la aplicación).
Si suponemos que de la lista anterior, tan sólo 1000 van a estar en memoria continuamente, entonces aplicamos la siguiente fórmula:
dwTamañoMinimo = (256 + 32) * 1.000) = 288.000 = 281,25 KB
Yo utilizaría un tamaño mínimo de 300 KB, para asegurarnos que el montón tendrá memoria comprometida inicial para albergar todos los objetos que se cargan en el arranque.
Este (junto con el tamaño de pila), puede ser un buen método para optimizar el tiempo de carga de una aplicación.
¿Cuántos montones debo crear?
Un programador principiante, las únicas variables que utilizará son las almacenadas en la pila (locales) o las variables estáticas (globales). Según su conocimiento va aumentando, aprenderá a utilizar la asignación dinámica de memoria, sin embargo, lo que suele hacer es realizar todas las reservas en el montón por defecto, ya sea utilizando funciones del lenguaje (malloc, GetMem, etc.), como funciones para montones locales y globales (LocalAlloc y GlobalAlloc). Un programador experto debe ir más allá, y detectar las situaciones en que es recomendable crear montones adicionales.
Utilizar múltiples montones puede incrementar el rendimiento, sobre todo poniendo énfasis en los siguientes puntos:
Un montón por cada hilo
En situaciones de acceso masivo al montón, se puede producir un cuello de botella cuando múltiples hilos acceden al montón por defecto repetidas veces (miles o millones). En estas situaciones es recomendable crear un montón por cada hilo, y realizar todas las peticiones de memoria al montón privado de cada hilo. Además, en este caso se puede (y se debe) desactivar el mecanismo de sincronización de hilos (utilizando la bandera HEAP_NO_SERIALIZE), ya que será un solo hilo el que haga accesos a cada montón, y este mecanismo ralentiza la ejecución.
Suponiendo que estamos programando un programa servidor, podríamos crear un hilo que gestione las peticiones de cada cliente que se conecta a nuestro servidor. Además, si estos hilos hacen un uso intensivo de la memoria dinámica, es muy recomendable crear un montón para cada uno de ellos, utilizando la bandera HEAP_NO_SERIALIZE en la llamada a HeapCreate.
Un montón para cada tipo de dato
Si utilizamos el montón por defecto para almacenar estructuras de datos, lo más probable, como ya hemos explicado, es que después de las primeras asignaciones/liberaciones, tengamos un montón con bloques de memoria fragmentados. Para aclarar esto, nada mejor que un ejemplo: supongamos que tenemos un montón 140 KB y hacemos las siguientes reservas:
- Reservar 20 KB
- Reservar 10 KB
- Reservar 50 KB
- Reservar 40 KB
Después de estas reservas, el aspecto del montón será el de la siguiente figura:

Como puede verse, los bloques libres (aunque en realidad la memoria virtual que los soporta está reservada) se representan en blanco y los bloques reservados, en gris. Si después de estas operaciones liberamos el bloque de 10 KB, el aspecto del montón será el como se muestra a continuación:

Este montón está fragmentado, ya que el espacio libre total es de 30 KB, pero si intentamos hacer una reserva con este tamaño, no lo conseguiremos, porque que no hay bloques contiguos suficientemente grandes. Si fuera un montón auto-extensible (como el montón por defecto), y se realizase una reserva de 30 KB, se tendría que crear un montón-hijo para acomodar este espacio, lo cual es una operación muy lenta.Esta situación, se podría evitar si utilizamos un montón por cada tipo de dato a almacenar.
Supongamos que necesitamos almacenar dos listas enlazadas: la primera de elementos de 5 KB y la segunda de elementos de 7 KB. Si ambas listas se almacenan en el mismo montón, podríamos llegar fácilmente a situaciones como la descrita. Sin embargo, si utilizamos un montón para cada lista, los bloques siempre serán lo suficientemente grandes, porque los "huecos" serán siempre de un tamaño múltiplo del espacio requerido (5 y 7 KB respectivamente) y los nuevos bloques siempre "encajarán" en estos "huecos". En esta figura

se muestra un montón fragmentado, pero que acomodaría perfectamente cualquier petición de 7 KB (representa el montón adicional para la segunda lista enlazada).
Situar los bloques de memoria próximos
Es conveniente que los bloques de memoria que vayan a ser utilizamos a la vez se reserven dentro de un rango de direcciones virtuales lo más pequeño posible. Esto es debido a que, cuando el sistema necesita más memoria, vuelca cierto rango de páginas al archivo de intercambio para dejar espacio libre en RAM. Si los bloques de memoria que necesitamos no están próximos entre sí, puede darse el caso de que nuestros datos hayan sido volcados al archivo de intercambio, con lo cual sería necesario volverlos a recuperar de disco y proyectarlos en memoria, lo cual es una operación muy lenta. Utilizando un montón para cada estructura de datos, conseguimos que los bloques de memoria que se van a utilizar a la vez se direccionen juntos, con lo que minimizamos el riesgo de que algunos de ellos sean volcados al archivo de intercambio.
En el ejemplo que pusimos anteriormente, es recomendable que los bloques de ambas listas enlazadas, se sitúen próximos entre sí dentro del sistema de memoria virtual, y esto se consigue utilizando un montón para cada una de ellas
Proteger componentes
Si en el mismo montón, mezclamos bloques de memoria de distintas estructuras, corremos el riesgo de que una escritura errónea en la manipulación de una de ellas, pueda afectar a los datos de la otra. En nuestro ejemplo, si cometemos un error al manipular la primera lista enlazada, podemos sobrescribir datos de la segunda lista, lo cual sería difícil de depurar, máxime si ambas estructuras se utilizan desde partes muy distintas del programa. Es mucho más conveniente aislar cada una de las estructuras en su propio montón, para evitar así que un error en una parte de un programa, afecte a sus datos, y no a los datos de otros objetos.
Este método es especialmente recomendable para proteger componentes encapsulados dentro de una DLL, ya que así nos aseguramos que no corromperemos la memoria del programa, sino la de nuestra propia DLL.
Conclusión
En este artículo hemos entrado en profundidad (y mucha) sobre este aspecto, tan importante como desconocido, de la arquitectura de memoria en Win32.
Hemos visto la importancia de los montones para la asignación dinámica de memoria, así como el uso interno que se hace de esta estructura desde cualquier lenguaje de programación.
También, hemos explicado la importancia y el modo de crear montones dinámicos, las funciones para su manipulación y las situaciones en las que es recomendable hacer uso de esta característica.
Espero que todo haya quedado suficientemente claro, y si no, ya sabéis que estoy disponible para cualquier duda en mi correo electrónico.
Los ejemplos
Todo lo que hemos ido explicando, se utiliza de modo práctico en el siguiente ejemplo: