Rust Memory
Explicando para mim mesmo, como a memória do Rust funciona.
Introdução
Acho bem legal como o Rust funciona, alocar memória na heep sem precisar de um garbage collector ou desalocar a memória na mão. É um assunto relativamente velho no mundo da programação, mas existem muitos iniciantes para quem posso contar como fnciona.
Como a memória funciona.
A memória do programa é separada em várias partes, para esse tutorial apenas entenda que existe 2 delas, sendo a stack e a heep. Stack significa pilha em ingles, e é exatamente que representa, uma pilha. Imagine como uma pilha de pratos, o último a ser empilhado é o primeiro a sair, ou seja, o primeiro a ser adicionado é o último a sair. esse conceito é chamado de “last in, first out”
Por isso a stack funciona empilhando a memória, é assim que chamadas a funções e simples variáveis são armazenadas. Para dados simples a stack é suficiente, tudo nela é organizado um atrás do outro. Na stack tudo é mais simples, é fácil prever onde os dados estão, já tudo é apenas empilhado, um ao lado do outro. O stack tem um limite raso, caso o ultrapasse, é resultado um erro de StackOverflow (A pilha transborda), e o programa fecha. Para armazenar na stack, ainda é necessário saber o tamanho exato da memória que estamos alocando, tamanhos que variam não podem ser armazenados na stack.
No outro lado temos a Heap, que é responsável por armazenar dados maiores e mais complexos. A Heap é bem maior do que a Stack, ao preço de ser menos organizado. Para guardar os dados na heep é necessário antes alocar (ou reservar) o espaço que queremos usar, para nenhum outro programa acabar esbarrando nos nossos dados. O Alocador vai procurar na heap, algum lugar que tenha o espaço suficiente que precisa, quando o acha, reserva esse espaço e retorna um ponteiro para o primeiro byte desse espaço, então, nosso programa está livre para utilizar esse espaço. Esse ponteiro é armazenado na stack, já que todo ponteiro vai ter o mesmo tamanho é permitido ficar aqui.
Espaços alocados na Heep continuam alocados mesmo após a variával parar de ser usada, já que, diferente da stack, a heep não é tão organizada. Ou seja, precisamos desalocar esse espaço, basicamente dizer ao sistema operacional que não estamos mais usando aquele lugar, e que pode liberar-lo para outro usar..
Como a memória é limpa
A forma manual
Em linguagens como C e C++, é necessário explicitamente dizer que o espaço não está mais em uso. Isso é trabalhoso, já que devemos monitorar nossas variáveis para saber quando estamos seguros de as remover, e basta esquecer de desalocar alguns espaços que seu programa começa a usar mais memória do que o necessário. Esse problema percorreu programadores por um tempo
Garbage Collectors
Uma ideia para solucionar esse problema é usando Garbage Collectors, pense neles como Lixeiros, que passam pelo programa procurando variáveis que estão sem uso, quando é locadalizada, esse espaço de memória é marcado e depois limpo. Isso faz com que o programador não tenha que manualmente desalocar memória, e agora pode focar mais no código, deixando tudo para um “robo” que limpara a bagunça. Porem com isso vem uma grande desvantagens, os chamados “Stop the World”, é que quando a aplicação ativa os garbage collectors para limpar a memória, como você deve ter imaginado, varrer seu programa em busca de lixo custa processamento, mesmo que seja feito em paralelo gera uma lentidão no programa. Isso pode ser uma desvantagem em algumas aplicações, mas não chega a ser alarmante. Um Garbage collector mais ativo significa maior uso de processamento, porem, menor uso de memória. Um menos ativo significa que terá mais uso de memória, porem menor uso da CPU. Uma faca de 2 gumes.
Como o Rust faz
A ideia do rust é linda. Uma variável é limpa quando chega ao fim do escopo. Simples assim. Valores alocados na heep pelo Rust possuem apenas um ínico dono, quando o dono do valor fica fora de escopo, seu conteúdo é limpo. Um problema óbvio pode ser “E se eu passar a variável como parâmetro de uma função, como garantir que o valor não será limpo antes do necessário”. É daí que vem a ideia de dono do Rust.
fn main(){
{
let s = String::from("Rust is amazing");
println!("{}", s);
} // "s" will be clear here.
}
Quando você passa uma variável como parâmetro, você está dando a variável para a função, ou seja, sua variável no escopo atual não possui mais o conteúdo, e agora, ela pertence ao escopo da função, e quando chegar ao fim, a memória será limpa sem problema algum. Como a variável perdeu seu conteúdo, ela não será mais usada, isso faz com que não exista erro para uma variável com um ponteiro para um local vazio na memória.
fn main(){
let s = String::from("Rust is amazing");
take_owner(s); // Takes the value
println!("{}", s); // Error, variables is has no value.
}
fn take_owner(s: String){
// Do something...
} // will clear here!
Mas isso gera um problema, perder a variável ao usa-la como parâmetro não parece tão útil. E não é mesmo! Por isso existem alternativas. Você pode clonar o conteúdo da variável na heep, e desta forma, gerar 2 objetos identicos, um deles para a função e outro para seu escopo atual, o custo disso é dúplicar a memória. Desta forma, existe 2 variáveis identicas, cada uma idependente da outra. Desta forma, cada uma pode ser limpa separamente e nada vai quebrar.
fn main(){
let s = String::from("Rust is amazing");
take_clone(s.clone()); // Takes a Clone of "s".
println!("{}", s); // This Works
} // Will Clear here.
fn take_clone(s: String){
// Do something...
} // The clonned "s" will clear here.
Outra solução, que evita duplicar memória é usar apenas uma referencia para a variável. Porem precisa garantir que viverá o tempo suficiente.
fn main(){
let s = String::from("Rust is amazing");
take_reference(&s); // Takes a referemce.
println!("{}", s); // This Works
} // Will Clear here.
fn take_reference(s: &String){
// Do something...
} // Only the pointer to "s" will clear here.
fn main(){
let s = String::from("Rust is amazing");
let s = take_and_give_back(s); // Takes the owner.
println!("{}", s); // This Works
} // "s" Will Clear here.
fn take_and_give_back(s: String) -> String{
// Do something...
return s; // Return the owner
}