5 - Representación en memoria

Comprender cómo se ubican los datos en la memoria RAM nos permite anticipar el costo real de cada operación. No basta con saber cuántos elementos contiene una estructura; también debemos conocer si están juntos, qué metadatos adicionales transportan y cuánto tarda la CPU en traerlos a los registros.

La representación también determina cuánto provecho obtenemos de la caché de CPU y cómo se comportan los algoritmos bajo presión. Un diseño acertado evita realocaciones costosas y reduce errores de manejo de punteros, dos de los problemas más frecuentes cuando pasamos de ejemplos académicos a sistemas reales.

5. Representación en memoria

El objetivo de este tema es comparar diferentes disposiciones físicas, entender cómo los punteros describen relaciones entre nodos y reconocer el impacto directo sobre la velocidad. Cada subsección contiene ejemplos prácticos y criterios para elegir la opción adecuada en tus proyectos.

5.1 Memoria contigua vs no contigua

Cuando hablamos de memoria contigua nos referimos a bloques consecutivos de direcciones. Es el caso de los arrays estáticos o de estructuras como std::vector, que reservan segmentos completos para garantizar acceso directo. Las estructuras no contiguas, como las listas enlazadas, pueden ubicar cada nodo en cualquier parte del heap y conectarlos a través de referencias.

  • Ventajas contiguas: cálculo fácil de índices, mejor aprovechamiento de la localidad espacial y mayor tasa de aciertos en caché.
  • Ventajas no contiguas: crecimiento flexible, inserciones y eliminaciones sin desplazar grandes bloques y posibilidad de mezclar nodos de tamaño variable.
  • Compromisos: la gestión de huecos libres en memoria dispersa puede fragmentar el heap, mientras que el uso de bloques contiguos exige planificar ampliaciones antes de que se llenen.

5.2 Punteros y referencias

Los punteros son direcciones que apuntan al siguiente dato o al inicio de un buffer. Para las estructuras no contiguas funcionan como el pegamento que mantiene todo unido, motivo por el cual debemos validar cada referencia antes de usarla. Las referencias, comunes en lenguajes como C++, son alias seguros que evitan reasignaciones accidentales pero siguen representándose internamente como un puntero.

int buffer[4] = {4, 8, 15, 16};
int *ptr = buffer;        // Dirección contigua
struct Nodo {
  int valor;
  struct Nodo *siguiente;
};
struct Nodo cabeza = {23, NULL};
struct Nodo otro = {42, NULL};
cabeza.siguiente = &otro; // Encadenamiento explícito

La manipulación de punteros requiere reglas claras: inicializarlos con valores conocidos, evitar dobles liberaciones y documentar la propiedad del recurso. En lenguajes gestionados (Java, C#) estos conceptos aparecen como referencias que el recolector de basura supervisa, pero los principios de acceso siguen siendo los mismos.

5.3 Relación entre memoria y velocidad de acceso

El tiempo de lectura depende de la distancia entre la CPU y los datos. Cuando un algoritmo recorre una estructura contigua explota la localidad de referencia: la caché carga líneas completas y las siguientes lecturas son casi instantáneas. En cambio, saltar entre nodos dispersos puede obligar a traer cada dirección desde la RAM, lo que multiplica la latencia.

  • Secuencial vs aleatorio: recorrer un array con un for aprovecha precarga masiva, mientras que acceder a índices aleatorios reduce la efectividad de la caché.
  • Sobrecalentamiento de bus: estructuras con muchos punteros requieren seguir referencias que no siempre están cerca, lo cual incrementa los fallos de TLB.
  • Microoptimizaciones: agrupar campos que se usan juntos (struct of arrays vs array of structs) mejora la densidad de datos relevantes que caben en una línea de caché.

5.4 Estructuras dinámicas vs estáticas

Una estructura estática reserva todo su espacio al inicio del programa. Es ideal cuando conocemos el tamaño máximo y queremos evitar sorpresas en ejecución. Las estructuras dinámicas solicitan memoria a demanda (con new, malloc o asignadores personalizados) y se amplían o contraen según los datos reales.

  • Estáticas: inicialización determinista, mejor compatibilidad con sistemas embebidos y cero fragmentación, pero poca flexibilidad.
  • Dinámicas: se adaptan a cargas variables y pueden devolver memoria al sistema, aunque requieren planeación de realocaciones y cuidadosa sincronización en contextos concurrentes.
  • Estrategia híbrida: reservar un buffer inicial contiguo y pasar a nodos enlazados cuando superamos el umbral deseado para obtener lo mejor de ambos mundos.

5.5 Costos ocultos: fragmentación y realocación

Incluso cuando una estructura ofrece buenas complejidades teóricas, la implementación concreta puede pagar peajes silenciosos. La fragmentación de memoria reduce la cantidad de bloques grandes disponibles y obliga al sistema operativo a mantener mapas extensos de espacios libres.

Las realocaciones también son costosas: copiar miles de elementos para duplicar la capacidad de un vector implica tiempo lineal y puede invalidar punteros existentes. Por eso es recomendable exponer funciones como reserve() o parámetros de factor de carga que permitan a los usuarios planificar el crecimiento.

Otros costos disfrazados incluyen inicializaciones perezosas, padding para alinear estructuras y los guardias que agregamos para detectar corrupción de memoria. Todos ellos ocupan bytes y pueden volver impredecible la distancia entre dos nodos, afectando al recolector de basura o a las rutinas de serialización.