[ LOW-LEVEL ] Memória: Stack e Heap
Alocação de memória, memória, em geral, no computador, é algo que sempre tive vontade de estudar. Um dos assuntos que eu era ansioso para estudar mas eu sempre procastinava esse estudo.
Segui este link que foi disponibilizado pelo @maniero, e me rendeu (ainda redendendo) vários dias de leitura e anotações. Acho que posso chamar isso de estudo.
Bem, o objetivo aqui é explicar o que eu entendi sobre o assunto e, consequentemente, aprender tanto ensinando, quanto vocês também podem me corrigir em algo que eu errar aqui - inclusive eu peço que deixe as suas conclusões sobre a minha visão deste assunto.
Stack
Traduzinho do inglês, significa pilha. Pilha porque é a forma que os dados são armazenados aqui, usando o método LIFO - last in, first out; último a entrar, primeiro a sair. Ou seja, os dados aqui são sempre colocados no topo e os dados removidos são sempre os do topo.
A Stack é conhecida por sua alocação automática, já que, ela mesma faz alocação e também a desalocação, fazendo com que a única preocupação do programador seja fornecer o dado a ser associado.
O seguinte código é em C:
int x = 1;
Isso é uma simples alocação na Stack, onde é pego um espaço na memória (o topo da Stack) e armazeno o dado.
Pontos a serem destacados:
- Stacks estão dentro de threads. Cada thread no seu programa possui uma Stack.
- Os dados são empilhados. Logo, podemos dizer que os dados são sequenciais
- Não é recomendado armazenar dados de grandes tamanhos, podendo causar o famoso stack overflow
- É usado um ponteiro - stack pointer - para alocar e desalocar dados
- Os dados que são armazenados na Stack podem ser: retorno de funções, paramêtros de funções e variáveis locais
- A desalocação é feita no término do programa, ou seja, de forma automática
Antes de ir pro Heap, quero explicar melhor o 2° ponto, quando disse:
"Os dados são empilhados. Logo, podemos dizer que os dados são sequenciais"
Quis dizer que o seguinte é possível:
int variable_1 = 1;
int variable_2 = 2;
int *pointer = &variable_1; // pega o endereço de 'variable_1'
printf("variable_1 = %d", *pointer); // output: variable_1 = 1
printf("variable_2 = %d", *(pointer + 1)); // output: variable_2 = 2
Heap
Heap, diferente da Stack, não possui um modelo. O que quer dizer que ele não possui bem uma "ordem" de alocar os dados nem de remover/desalocar.
O Heap é flexível. Mas, por não possuir um padrão/modelo de alocação e desalocação, acaba que o Heap perde enficiência.
Também chamado de dinâmico, já que é possível alocar e desalocar durante a excução do programa. Ou seja, é possível pedir para o usuário informar o tamanho do dado e armazenar um espaço na memória a partir desse dado fornecido pelo usuário - mesmo que não seja tão eficiente.
Ficaria assim, em C:
void alocar_com_heap(int tamanho_do_dado) {
int *num = malloc(sizeof(int) * tamanho_do_dado); // reservando um espaço na memória
*num = 1;
printf("num = %d", *num); // output: num = 1
}
Tá certo. E pra liberar uma memória no Heap, como faz?
Tem as seguintes formas:
- Forma manual
- Garbage Collector - estudando
Bem, no C, o recomendando é sempre usar free()
- que seria a forma manual - para não acontecer nenhum vazamento de memória, ou seja, a memória continua alocada mesmo após o término do programa.
Então, para completar o código de lá de cima:
void alocar_com_heap(int tamanho_do_dado) {
int *num = malloc(sizeof(int) * tamanho_do_dado); // reservando um espaço na memória
*num = 1;
printf("num = %d", *num); // output: num = 1
free(num); // liberando a reserva no heap
}
Conclusão
A minha visão sobre o assunto é essa.
Faz pouco tempo que eu estou estudando - comecei na segunda - então, a ideia ainda vai "amadurecer" na minha mente.
Também quero que esse post sirva de incentivo para você que está no início da aprendizagem em programação (eu já tenho um bom tempo que tento estudar sobre a área, então não estou tão no início) para que aprenda mais sobre a base, conceitos low-level.
Quero destacar o seguinte: eu aprendi, deixei mais claros alguns conceitos sobre este assunto só de escrever este post. Fica aí a dica.
Assuntos para Estudo:
Tem vários assuntos que não foram abordados aqui, e o porquê disso é que o assunto ainda está sendo processado pela minha mente. Ou, simplesmente, ainda estou estudando. risos.
Alguns deles:
Estude todo dia um pouco. Isso funciona de verdade.
Sabe o que é engraçado? Programadores que vão manipular a pilha no baixo nível sabem que elas não são bem pilhas. Eu sempre omito isso, mas é o contrário. Vai de cima para baixo, é uma pilha morcegando :D
A Stack é conhecida por sua alocação automática, já que, ela mesma faz alocação e também a desalocação
Eu posso ter dado a entender isso. Mas na verdade a alocação em si é fixa da pilha toda. Ela deixa você usar um espaço dela e volta atrás quando não precisa mais daquilo. De fato é automaticamente que isso acontece. Quando inicia um escopo que tem um frame um ponteiro (stack pointer) "sobe" (de verdade, desce, mas é mais fácil enxergar como subindo) na pilha, e quando não precisa mais esse ponteiro volta para o lugar original, marcando onde é o topo da pilha naquele momento (o tipo lógico, o físico é fixo - em tese também, mas naõ vamos complicar).
O primeiro código no exemplo entra na pulha se estiver em uma função. É raro usar for,a mas se for a alocação não é na pilha.
A liberação do espaço em cada frame é no fum da função. A desalocação da puilha como um todo é no fim do programa. Isso acontece em C e quase todas as outras linguagens. Alguém pdoe inventar alguma maluquice.
Após o térmido do programa a memória é sempre liberada pelo sistema operacional, não tem risco. Em tese se você sabe que o programa vai rodar por pouco tempo e aloca pouco dinamicamente, pode até nmão usar o free()
que estará tudo bem. Mas pra que fazer isso? Até tem caso que faz sentio, mas não faça por preguiça, acostume-se fazer o mais certo.
O problema é que sem o free()
enquanto executa vai acumulando e vai estourar a a RAM, vai ficar muito lento (vai para a memória virtual em disco (SSD)), até quebrar de vez em casos extremos.
O exemplo mostrado está ok, mas é óbvio que fazendo assim não tem como se enrolar, o problema que a alocação assim costuma ser na pilha mesmo, se usa a forma dinâmica onde aloca em um lugar e libera em outro, aí começa ficar difícil, por isso o GC é uma técnica mais fácil, apesar de conbrar um preço.
Saber essas coisas ajuda programar melhor qualquer coisa, mesm oque nunca use direrentamente. Desde que a pessoa estude do jeito certo, tem gente que não consegue fazer associações depois.
Ajudei? Era o meu desejo.
Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente (não vendo nada, é retribuição na minha aposentadoria) (links aqui).
Obrigado pelo crédito.
Ajudei? Era o meu desejo.
Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente (não vendo nada, é retribuição na minha aposentadoria) (links aqui).
Muito bom esse conteúdo, hoje em dia com linguagem que não usam tipagem de dados, ninguém tem noção desses escopos de memória, simplesmente vão e fazem. Eu venho do java e posso dizer que para se ter performance você tem que entender essas partes e até um fine tunning com flags pro garbage collector é um assunto extenso. Na minha opinião a evolução do Hardware, as linguagens começaram abstrair muito esse assunto, e dai surgiram a escalabilidade pra contornar esse tipo de problema. Acredito muito que se tivessemos melhores programadores desse nível, não precisariamos ter tanto de escalibidade etc. Da uma olhada em rust tbm, é uma filosofia legal mas também tem uns conceitos estranhos kkk. Obrigado por compartilhar, vou acompanhar.
Ótimo post!
Se não me engano a heap é acessível em todos os threads e tem escopo global. E ela pode estar tanto na ram quanto no hd/ssd.
Cara muito bacana a explicação!
Quero adicionar aqui como isso é um pouco diferente em rust. Em rust a desalocação ocorre quando, por exemplo, uma variável sai de escopo.
fn exemplo(x: i32) {
// usa o x para qualquer coisa
}
fn main() {
let num = 42;
exemplo(num);
println("exemplo = {}", num); // Erro!
}
O código acima na maioria das linguagens não teria problemas. Em rust, por conta de algumas regras no compilador, nem compila esse código pois a variavel 'num' foi entregada para a função 'exemplo' e isso faz com que essa função agora seja 'dona' da variável. Então assim que a função termina a variável foge de escopo e ela é automáticamente desalocada, por isso não é possível fazer o print depois.
Só isso já evita muitos erros que poderiam acontecer envolvendo alocação e acesso a memória, e acho rust bem bacana por se preocupar com essas coisas.