Por que seu aplicativo precisa mais do heap do que da pilha

12

Seu computador não possui apenas RAM. Ele tem um cenário de memória confuso, fragmentado e em constante mudança que a CPU finge ser infinita. Esta é a realidade das estruturas de dados dinâmicas executadas em hardware moderno.

Uma estação de trabalho típica hoje tem de 16 a 64 megabytes de RAM física. Mas não se deixe enganar por esses pequenos números. Através de uma técnica chamada memória virtual, o sistema troca dados entre essa RAM e o disco rígido. A CPU vê uma ilusão. Ele acha que tem de 200 a 500 megabytes de espaço contíguo. A ilusão funciona. O código é executado. Mas quando o sistema operacional precisa vasculhar o disco rígido para buscar uma página de memória, tudo fica mais lento. Você sente isso no atraso. Você sente isso no giro do ventilador. Apesar do impacto no desempenho, a memória virtual é uma forma barata de “expandir” sua RAM. É um compromisso necessário.

Vamos supor uma lousa em branco. Um espaço de memória total de 50 megabytes. Não mais. Nada menos. Este é o orçamento.

O sistema operacional possui este bloco de 50 megabytes. Isso divide o bolo. Não igualmente. Nunca igualmente.

Primeiro, existe o código. As instruções executáveis ​​para cada aplicativo atualmente em execução, além do próprio kernel do sistema operacional. Esta seção é estática. Isso não muda enquanto o programa está ativo. Em seguida vêm as variáveis ​​globais. Estas são as constantes compartilhadas e sinalizadores de estado que cada parte do aplicativo pode tocar. Eles ficam na memória desde o momento em que o aplicativo é iniciado até que ele morra.

Depois, há a pilha.

A pilha é rígida. Ele cresce e diminui em sincronia com chamadas de função. Quando você invoca uma função, suas variáveis ​​e parâmetros locais são colocados na pilha. Quando a função retorna, eles são removidos. A pilha lembra a ordem de chamada. Isso garante que você retorne ao lugar certo. Mas tem um tamanho máximo. Você não pode solicitar mais memória à pilha durante o voo. Se você tentar, a pilha transborda. O programa trava.

Quando um programa termina, o sistema operacional o descarrega. O código, os globais, o espaço da pilha – tudo isso é apagado. Essa memória é reciclada. Está pronto para o próximo programa.

Mas aqui está o problema. A qualquer momento, cerca de 50% desse espaço de 50 megabytes pode não ser utilizado. Por que? Porque a pilha contém apenas o que está em execução no momento. O código contém apenas as instruções. Os pedaços de memória não utilizados estão espalhados. Eles são buracos.

O sistema operacional pega essas lacunas e as agrupa. Ele chama esse pool coletivo de heap.

É aqui que acontece a alocação dinâmica. O heap é o único lugar na memória onde um programa pode solicitar uma quantidade exata de espaço, precisamente quando precisa. Você não declara um array de 1.000 inteiros em tempo de compilação. Você não sabe se precisará de 1.000 ou 100.000. Você espera. Você executa o código. Você decide que precisa desse espaço. Então você chama malloc.

malloc significa alocação de memória. Ele pega um bloco da pilha. Ele retorna um ponteiro. Você usa esse ponteiro. Quando terminar, você chama free. O bloco volta para a piscina.

Esta é a diferença fundamental entre memória estática e dinâmica. A pilha é automática. Está vinculado ao escopo. A pilha é manual. Está ligado à intenção.

Por que isso é importante para você? Porque a maioria dos bugs em softwares complexos não vem de uma lógica incorreta. Eles vêm do mau gerenciamento de memória. Se você alocar do heap e esquecer de liberá-lo, haverá vazamento de memória. A pilha diminui. Eventualmente, o sistema fica sem espaço. Os programas congelam. O sistema operacional mata processos. Você perde o trabalho não salvo.

Por outro lado, se você tentar alocar muito na pilha, você ultrapassará o limite da pilha. O programa termina abruptamente.

O heap permite flexibilidade. Ele permite estruturas de dados que crescem. Listas vinculadas. Árvores. Gráficos. Nada disso pode ser definido em tempo de compilação com um tamanho fixo. Eles devem ser construídos dinamicamente. Eles devem viver na pilha.

A pilha é perfeita? Não. Ele se fragmenta. À medida que você aloca e libera blocos de tamanhos diferentes, a pilha se torna um quebra-cabeça com peças faltando. Encontrar um bloco contíguo de memória livre pode ser lento. É por isso que malloc às vezes pode levar um tempo considerável. Tem que pesquisar. Tem que se unir. Tem que administrar o caos.

Mas sem a pilha, a computação moderna entra em colapso.