Ciencias de la computación

Stack vs Heap: dónde se almacenan las variables en Swift

La pila de memoria se limpia con las llamadas a funciones, mientras que el heap admite tamaños y vidas dinámicos. Resumimos el coste de ambas áreas, la disposición real de los tipos por valor y referencia en Swift y sus diferencias frente a las estructuras Stack y Heap.

5 min de lectura
Imagen de portada de Stack vs Heap: dónde se almacenan las variables en Swift

Al declarar una variable, ¿dónde se almacena su valor?

Responder «en la memoria» es acertado a medias. Incluso dentro de la memoria, el destino de los valores que van a la pila y de los que van al heap es completamente distinto.

El error «stack overflow», las diferencias de rendimiento entre clases y estructuras y la necesidad de ARC: todo se explica entendiendo estas dos áreas.

En este artículo resumimos cómo funcionan la pila y el heap, por qué difieren en velocidad y dónde termina cada cosa en Swift.

Aquí hablamos de las áreas de memoria. La estructura LIFO se trata en Pila y cola en Swift, y la estructura arbórea que implementa una cola de prioridad, en Heap y cola de prioridad.

Veamos primero el resumen clave.

  1. Pila: área gestionada automáticamente, que crece con las llamadas a funciones y desaparece al retornar. Es rápida.
  2. Heap: área dinámica que toma prestado el tamaño necesario durante la ejecución. Es flexible, pero tiene costes de gestión.
  3. La esencia de la diferencia de velocidad está en «cómo se asigna y libera» y «quién realiza la limpieza».
  4. En Swift, las estructuras suelen vivir en la pila y las instancias de clase, en el heap.

Pila: platos apilados

La pila almacena la información de las llamadas a funciones. Como su nombre indica, funciona como una pila de platos: el último que se coloca es el primero que se retira, una estructura LIFO.

Al llamar a una función, sus variables locales, parámetros y dirección de retorno se apilan juntos en un marco de pila. Cuando la función retorna, el marco desaparece por completo.

Aquí aparece la principal ventaja de la pila.

La asignación es rápida. Basta con mover hacia arriba el puntero de pila. No hace falta buscar espacio libre.

La liberación también es gratuita. Al terminar la función, solo hay que devolver el puntero. No hace falta decidir quién limpia nada.

Pero tiene limitaciones. El tamaño debe conocerse en compilación, el dato desaparece al terminar la función y el límite total es pequeño (normalmente, varios MB). Si una función recursiva se llama indefinidamente sin condición de salida, los marcos superan ese límite: es el stack overflow.


Heap: un almacén amplio que requiere gestión

El heap es el área de la que se toma prestado lo necesario durante la ejecución. No hace falta conocer el tamaño de antemano, los datos sobreviven al final de la función y hay mucho más espacio.

A cambio, implica costes de gestión.

La asignación es relativamente lenta. Hay que encontrar espacio libre del tamaño solicitado y coordinarse si varios hilos asignan memoria a la vez.

Alguien debe encargarse de liberar la memoria. Como no desaparece al terminar la función, alguien debe decidir cuándo «ya no se usa». En C, el programador llama directamente a free; Java limpia periódicamente con el recolector de basura; Swift libera cuando ARC reduce el recuento de referencias a cero.

Un error de criterio provoca dos problemas. Liberar demasiado tarde causa una fuga de memoria; hacerlo demasiado pronto, acceder a memoria liberada (un puntero colgante).

Las referencias del marco de pila apuntan al objeto real del heap
Las referencias del marco de pila apuntan al objeto real del heap

¿Qué va a cada área en Swift?

La documentación oficial de Swift y las sesiones de WWDC repiten una idea: por defecto, los tipos por valor (estructuras y enumeraciones) se asignan en la pila, mientras que los tipos por referencia (clases) se asignan en el heap.

struct PointStruct { var x, y: Double }
class PointClass { var x = 0.0, y = 0.0 }

func run() {
    let a = PointStruct(x: 1, y: 2) // íntegramente dentro del marco de pila
    let b = PointClass()            // asignada en el heap; en la pila, solo la referencia
}

El valor de la estructura a entra íntegramente en el marco de pila. Al terminar la función, desaparece con él, por lo que no necesita recuento de referencias.

La instancia de clase b se ubica en el heap y en la pila solo queda su dirección. Como puede tener referencias desde varios lugares, ARC debe contar esas referencias.

Esta es la base de rendimiento de la recomendación de Apple de «considerar primero las estructuras»: se eliminan los costes de asignación en el heap, recuento de referencias y bloqueos.

Sin embargo, «estructura = siempre pila» es incorrecto. Una estructura almacenada como propiedad de una clase vive en el heap junto con ella. Tipos como String y Array parecen estructuras, pero mantienen sus búferes de datos reales en el heap. Lo preciso es decir que una estructura puede cumplir las condiciones para colocarse en la pila.

La estructura está íntegra en la pila; la clase guarda su objeto en el heap y solo deja la referencia en la pila
La estructura está íntegra en la pila; la clase guarda su objeto en el heap y solo deja la referencia en la pila

Preguntas habituales en entrevistas

«¿Por qué la pila es más rápida que el heap?» — Porque en la pila asignar y liberar consiste solo en mover un puntero; el heap necesita buscar espacio libre y gestionar la liberación (recuento de referencias y GC).

«¿Por qué se produce un stack overflow?» — El límite de la pila es pequeño; una recursión profunda o variables locales enormes hacen que los marcos lo superen.

«¿Las variables locales están siempre en la pila?» — No. Si una variable local es de un tipo por referencia, el objeto está en el heap. En la pila solo queda la referencia.


Resumen

  • La pila crece y desaparece automáticamente con cada llamada a función. Se gestiona moviendo un puntero, por eso es rápida.
  • El heap se reserva durante la ejecución con el tamaño necesario. Es flexible, pero cuesta buscar espacio y gestionar la liberación.
  • Responsable de la liberación: la pila, automáticamente; el heap depende del lenguaje (C, manual; Java, GC; Swift, ARC).
  • El stack overflow ocurre cuando la pila supera su límite, por ejemplo, debido a una recursión profunda.
  • En Swift, las estructuras suelen estar en la pila y las clases, en el heap. Esa es la base de rendimiento para priorizar tipos por valor.
  • Pero las estructuras dentro de clases viven en el heap, y String y Array mantienen sus búferes internos allí.