Software de alto rendimiento

Qué es y cómo funciona la memoria «Stack» en .net (incluida una descripción general sobre StackOverflowException)

Prácticamente en todos los lenguajes de programación modernos, tenemos datos almacenados en dos regiones de memoria diferentes: Stack y Heap. Todavía, hemos notado que pocas personas saben cómo distinguir entre ellos.

En esta publicación, explicaremos Stack, em .NET, de forma incremental.

Hemos optado por un enfoque didáctico y, por supuesto, hemos omitido muchos detalles de cómo se implementa Stack en los lenguajes, frameworks y sistemas operativos modernos. Elegimos simplicidad sobre precisión técnica.

Comenzando con un ejemplo simple

El siguiente programa simplemente imprime un mensaje en la pantalla.

public class Program
{
    public static void Main()
    {
        System.Console.WriteLine("Hello, World!");
    }
}

Ahora intente imaginar cómo la computadora ejecuta este programa.

Para empezar, sería importante que la versión binaria (ejecutable) se cargue correctamente en la memoria. Correcto?

NOTA: En la práctica, en .NET (y Java), este no es el caso. Los programas .NET se cargan en una representación intermedia en lenguaje intermedio, y cada método se convertirá en código binario ejecutable solo cuando se ejecute por primera vez.

La computadora necesitaría mantener un «puntero» a la instrucción a ejecutar, y todas las instrucciones deberían ejecutarse secuencialmente desde la primera hasta la última.

Ejecutando un programa más complejo

El siguiente programa utiliza un enfoque recursivo (no optimizado) para calcular el valor de un elemento de la secuencia de Fibonacci.

public class Program
{
    public static void Main()
    {
        System.Console.WriteLine(GetNthFibonacci(10));
    }

    public static int GetNthFibonacci(int n)  
    {  
        if ((n == 0) || (n == 1))  
        {  
            return n;  
        }  
        return GetNthFibonacci(n - 1) + GetNthFibonacci(n - 2);  
    }  
}

Como antes, podemos imaginar este programa completamente cargado en la memoria.

El problema es que esta vez la ejecución es un poco más compleja. No podemos simplemente ejecutar las instrucciones del programa una tras otra hasta el final.

En el ejemplo, el método Principal espera un retorno de la función GetNthFibonacci , que a su vez se ejecuta recursivamente para llegar a una respuesta.

En cada ejecución (recursiva) de GetNthFibonacci, el valor del parámetro n es diferente. Además, cada vez que finaliza una llamada recursiva, el programa debe volver al punto en el que se produjo la llamada, con el retorno apropiado, recuperando los valores de las variables locales.

Para permitir que un método llame a otros, de forma recursiva o no, los programas usan estructuras de datos peculiares en una región distinta de la memoria: Stack.

¿Qué es la Stack?

La stack, en el contexto de esta publicación, es la estructura de datos preservada en una región distinta de la memoria que permite, entre otras cosas, que en nuestros métodos llame a otros métodos (funciones) y continúe sus ejecuciones tan pronto como se produce un retorno, preservando las variables locales.

Cada vez que llamamos a un método, se apila un «registro» ( Stack Activation Record) en esta estructura. En este registro están:

  • Los argumentos que se pasaron al método
  • La dirección del remitente. Es decir, la dirección de memoria donde se ejecutará la instrucción cuando el método complete su ejecución.
  • Las variables locales que se utilizarán en el método.

Cuando finaliza la ejecución de un método, este registro se desapila (libera la Stack), el puntero de ejecución se mueve a la posición de memoria indicada por la «dirección de retorno» y la ejecución continúa desde ese punto.

Cuando un método necesita consultar el valor de un argumento, va a su marco de pila y recupera ese valor. Lo mismo es cierto para las variables locales.

El valor de un argumento o variable local puede ser literal (como con los tipos primitivos y structs) o una referencia a un valor que se encuentra en otra región de la memoria (heap ).

Es importante destacar que todos los valores literales en un marco de pila se descartan tan pronto como se desapila.

La cantidad de memoria destinada a contener la pila a menudo es limitada. Cuando se invocan muchos métodos de cadena, se apilan varios marcos de pila, lo que finalmente agota la memoria disponible para la pila. En estos casos, en .NET, esto arroja una StackOverflowException .

No hay forma de evitar esta excepción . Después de todo, se ha excedido un límite. Si su programa está lanzando esta excepción, deberá repensar su implementación.

¿Qué es StackOverflowException?

La cantidad de memoria destinada a contener la Stack a menudo es limitada. Cuando se invocan muchos métodos de cadena, se apilan varios marcos de pila, lo que finalmente agota la memoria disponible para la Stack. En estos casos, en .NET, esto arroja una StackOverflowException .

Cuando se produce esta excepción, el programa finaliza. Después de todo, se ha excedido un límite. Si su programa está lanzando esta excepción, deberá repensar su implementación.

Conclusiones

En esta publicación, presentamos muy superficialmente qué es Stack y por qué es importante.

Como advertimos al principio, no nos preocupa el rigor técnico. Elegimos utilizar un enfoque didáctico. Esperamos que lo hayas disfrutado.

Deje sus preguntas y consideraciones en los comentarios.

Más publicaciones en la serie Software de alto rendimiento

Deja un comentario

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