Qué guarda la pila de llamadas
Cada llamada pausada ocupa un marco con sus datos y el punto al que debe volver. Ver esa pila explica la recursión, los stack traces y por qué un programa correcto puede morir con una entrada grande.
Para este tema conviene tener claro:Pensar en recursivo
Cuando una función llama a otra, la primera no desaparece: queda pausada a mitad de una línea, esperando un resultado. El programa tiene que recordar sus variables y el punto exacto donde retomar. Esa memoria ordenada de trabajo pendiente es la pila de llamadas, y entenderla explica tres cosas que parecen distintas: cómo funciona la recursión, cómo se lee un stack trace y por qué un programa correcto puede morir con una entrada grande.
Qué hay en un marco
Cada invocación crea un marco (stack frame). Conceptualmente guarda tres cosas: los parámetros, las variables locales y la dirección de retorno, que es el lugar del código que llamó y donde hay que seguir cuando esta función termine.
El marco nuevo se apila arriba. Cuando la función termina, su marco sale primero. Es una estructura LIFO: el último en entrar es el primero en salir.
Este es el programa que vamos a seguir durante todo el post:
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(factorial(4))Así se ve la pila en el momento de mayor profundidad, cuando factorial(1) está por
devolver:
Antes de seguir, predecí
La pila, llamada por llamada
Seguir la pila paso a paso es la forma más segura de predecir qué hace un programa recursivo. Antes de ejecutarlo, una pregunta:
Antes de seguir, predecí
Ahora comprobalo. Avanzá de a un paso con las flechas: a la izquierda se marca la línea que
se ejecuta y en gris las líneas donde quedaron pausados los marcos de abajo; a la derecha,
cada marco con su n. Cambiá n para ver cómo crece la pila.
Escena 1 — La pila crece y se vacía
paso a paso
Cargando la escena…
Profundidad y espacio
Lo que ocupa memoria no es la cantidad total de llamadas, sino cuántos marcos conviven al mismo tiempo. Esa cantidad máxima se llama profundidad de recursión:
La diferencia entre “llamadas totales” y “profundidad” es la que más se confunde. Mirá Fibonacci en su versión ingenua:
def fibonacci(n):
return n if n <= 1 else fibonacci(n - 1) + fibonacci(n - 2)fibonacci(5) hace 15 llamadas en total, y ese número crece como . Pero la
segunda rama recién empieza cuando la primera ya terminó y sacó sus marcos de la pila.
Antes de seguir, predecí
| Algoritmo | Llamadas totales | Profundidad máxima |
|---|---|---|
| factorial(n) | n | O(n) |
| Búsqueda binaria recursiva | O(log n) | O(log n) |
| fibonacci(n) ingenuo | O(2ⁿ) | O(n) |
| Recorrido de árbol balanceado | n | O(log n) |
| Recorrido de árbol degenerado | n | O(n) |
Cuando no entra otro marco
La pila tiene un tamaño fijo. Si la recursión no termina, o termina pero demasiado
profunda, llega un momento en que no entra otro marco y el programa aborta: es el
desbordamiento de pila (stack overflow). Python se ataja antes con su propio límite
de profundidad y lanza RecursionError.
La causa más común no es una entrada gigante, sino un caso base que nunca se alcanza:
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
factorial(-1)El otro caso es la recursión correcta pero profunda: recorrer recursivamente una lista enlazada de cien mil nodos necesita cien mil marcos, y CPython corta en mil por defecto. Si la profundidad depende de datos que no controlás, conviene una versión iterativa.
Leer un stack trace
Un stack trace es una foto de la pila en el momento del error. Esto es lo que imprime
Python con el factorial(-1) de arriba, simplificado:
Traceback (most recent call last):
File "factorial.py", line 6, in <module>
factorial(-1)
File "factorial.py", line 4, in factorial
return n * factorial(n - 1)
File "factorial.py", line 4, in factorial
return n * factorial(n - 1)
File "factorial.py", line 4, in factorial
return n * factorial(n - 1)
[Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceededMost recent call last: Python lo imprime con la llamada más reciente abajo. Arriba
está <module>, la base de la pila; cada bloque siguiente es un marco más arriba, y justo
antes del mensaje de error está el marco donde se rompió. Java y Node lo imprimen al revés,
con el más reciente primero. En un error normal, buscá el último marco que apunte a tu
código, no a una librería.
La pila en sistemas reales
Cada lenguaje decide cuánta pila hay y qué pasa al pasarse. Los números son órdenes de magnitud, porque dependen de la plataforma y de cuánto ocupa cada marco:
| Entorno | Límite típico | Qué pasa al pasarse | Cómo se ajusta |
|---|---|---|---|
| Python (CPython) | 1.000 niveles por defecto | RecursionError | sys.setrecursionlimit, con riesgo de que el intérprete se caiga |
| Node.js (V8) | Unos 10.000 marcos simples | RangeError: Maximum call stack size exceeded | Flag --stack-size, poco recomendable |
| Java (JVM) | Según la pila del hilo, del orden de 1 MB | StackOverflowError | -Xss, o el tamaño al crear un Thread |
| Go | La pila de cada goroutine crece sola, hasta 1 GB en 64 bits | fatal error: stack overflow | debug.SetMaxStack |
| C / C++ | La pila del hilo, suele ser 8 MB en Linux | Segmentation fault, sin mensaje | ulimit -s o atributos del hilo |
Más a fondo · nivel seniorRecursión de cola: por qué no conviene contar con ella
Una llamada está en posición de cola cuando no queda trabajo pendiente después de ella:
return f(x) sí, return n * f(x) no. En ese caso un compilador puede reutilizar el marco
actual en lugar de apilar uno nuevo, y la recursión usa espacio constante.
El problema es que esa garantía depende del entorno:
- Python: CPython no la hace, por decisión de diseño, para conservar stack traces completos.
- JavaScript: el estándar ES2015 la exige, pero en la práctica sólo la implementa Safari. V8, y por lo tanto Node y Chrome, no.
- Scala y Kotlin:
@tailrecytailrecconvierten la función en un ciclo al compilar, y fallan la compilación si la llamada no está realmente en posición de cola. Ahí sí es una garantía. - Scheme: la exige la especificación del lenguaje.
Consecuencia práctica: si el código tiene que correr en varios runtimes, escribir “de cola” no protege de nada. Un ciclo explícito sí.
Un detalle más para sistemas con muchos hilos: cada hilo tiene su propia pila. Mil hilos de plataforma en la JVM reservan del orden de mil megas de memoria virtual sólo en pilas. Es una de las razones detrás de las goroutines de Go y los hilos virtuales de Java, cuyas pilas empiezan chicas y crecen a demanda.
Cierre
Autoevaluación
¿Lo entendiste?
Práctica