Fundamentos para Performance

Entendendo a Stack (e a StackOverflowException)

Todas as linguagens modernas trabalham com duas regiões distintas de memória: Stack e Heap. Entretanto, temos percebido que poucas pessoas sabem apontar a distinição entre elas.

Nesse post, explicaremos a Stack, de forma incremental, a partir de seu propósito em dois cenários.

DISCLAIMER: Optamos por uma abordagem didática e, com certeza, omitimos muitos detalhes de como a Stack é implementada em linguagens, ambientes e sistemas operacionais modernos. Escolhemos simplicidade em lugar da precisão técnica.

Cenário 1 – Executando um programa simples

O programa que segue simplesmente imprime uma mensagem na tela.

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

Agora, tente imaginar como o computador executa esse programa.

Para começar, seria importante que a versão em binário (executável) estivesse devidamente carregada na memória. Certo?

NOTA: Na prática, em .NET (e em Java), não é isso que ocorre. Programas em .NET são carregados em uma representação intermediária, em Intermediate Language, sendo que cada método será convertido em código binário executável (assembly), apenas quando ocorrer sua primeira execução.

O computador precisaria manter um “ponteiro” apontando para a instrução a ser executada e, todas as instruções deveriam ser executadas sequencialmente, da primeira até a última.

Cenário 2 – Executando um programa mais complexo

O programa que segue utiliza uma abordagem recursiva (nada otimizada) para calcular o valor de um elemento da sequência 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);  
    }  
}

Da mesma forma como ocorreu anteriormente, podemos imaginar esse programa inteiramente carregado na memória.

O problema é que, dessa vez, a execução é um pouco mais complexa. Não podemos simplesmente executar as instruções do programa, uma após a outra, até chegar ao fim.

No exemplo, o método Main aguarda por um retorno da função GetNthFibonacci que, por sua vez, se executa recursivamente para chegar a uma resposta.

Em toda execução (recursiva), de GetNthFibonacci, o valor do parâmetro é diferente. Além disso, sempre que uma chamada recursiva se encerra, o programa precisa voltar ao ponto em que a chamada ocorreu, com o retorno apropriado, recuperando os valores das variáveis locais.

Para poder permitir que um método chame outros, recursivamente ou não, linguagens, ambientes e sistemas operacionais utilizam estrutura de dados peculiar, em uma região distinta da memória: a stack.

O que é a Stack?

A stack, no contexto deste post, é a estrutura de dados preservada em uma região distinta da memória que permite, entre outras coisas, que, em nossos códigos, métodos chamem outros métodos (funções), e continuem suas execuções assim que ocorrer um retorno, preservando variáveis locais.

Cada vez que chamamos um método, um “registro” (stack/activation frame) é empilhado nessa estrutura. Nesse registro estão:

  • Os argumentos que foram passados para o método
  • O endereço de retorno. Ou seja, o endereço de memória onde está a instrução que deverá ser executada quando o método concluir sua execução.
  • As variáveis locais que serão utilizadas no método.

Quando a execução de um método se encerra, esse registro é desempilhado (liberando a stack), o ponteiro de execução é movido para a posição da memória indicada pelo “endereço de retorno” e a execução continua daquele ponto.

Quando um método precisa consultar o valor de um argumento, ele acessa seu respectivo “stack frame” e recupera esse valor. O mesmo ocorre para variáveis locais.

O valor de um argumento ou de uma variável local poderá ser literal (como ocorre com tipos primitivos e structs) ou ser uma referência para um valor que está em outra região da memória (a heap, que é tema para outro post).

Importante destacar que todos os valores literais em um stack frame são descartados assim que este for desempilhado.

O que é a StackOverflowException?

O montante de memória destinado para manter a Stack costuma ser limitado. Quando ocorre a chamada de muitos métodos em cadeia, diversos stack frames vão sendo empilhados esgotando, eventualmente, a memória disponível para a Stack. Nesses casos, em .NET, isso gera uma StackOverflowException.

Não há como contornar essa exception. Afinal, um limite foi ultrapassado. Se seu programa está gerando essa exception, você precisará repensar sua implementação.

Concluindo…

Nesse post, apresentei, de forma bastante superficial, o que é a Stack e porque ela é importante.

Como advertimos no início, não nos preocupamos com rigor técnico. Optamos por usar uma abordagem didática. Esperamos que você tenha gostado.

Deixe suas dúvidas e considerações nos comentários.

Mais posts da série Fundamentos para Performance

4 Comentários
  1. Flavio Spedaletti

    Ótimo post!
    Como disseram, vale a pena escreverem sobre a heap também 🙂

    1. Elemar Júnior
  2. Leandro

    Interessante deixar algumas biografias como recomendação para estudos sobre os assuntos abordados.

  3. Jesse

    Elemar, em que situações, você sugere usar a alocação de memória Stack?

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *