What are stack and heap

Both stack and heap are used for memory storage of variables. For most programmers, there is no need to care how memory is allocated to heap and stack (in fact, the difference between stack and heap is not clear to many). In JavaScript, for example, most people know the conclusion that base data types are in the stack and reference data types are in the heap. But when asked why such a division is needed, few can say much.

So what’s the difference? First, although they are both memory available to code, the structure is different. A stack is a linear data structure that stores values in the order in which they are placed and retrieves them in the reverse order, called last in, first out. Its data input and take out, then is usually referred to as in and out of the stack. Since a stack is a linear data structure, all data in the stack must occupy a known and fixed size.

Heap, on the other hand, is a non-linear data structure that lacks organization. Therefore, some data whose size is unknown or may change at the beginning can be placed in the heap. When we want to store some data in the heap, we usually request is actually a suitable to the size of the space, then the operating system in the heap to search a large enough space to match what we request the amount of memory, and mark it as I use it, and return a said the location address pointer, * * * * and this pointer is often exist in the stack. This process is also known as heap allocation, often referred to as memory allocation.

It’s important to note that pushing variables onto the stack is not really an allocation, because it’s essentially just a sequential allocation, rather than an explicit allocation

Because their data structure is different, storage way is different. It also determines that pushing values onto the stack is faster than allocating memory on the heap. Because the operating system doesn’t need to search for memory space for new data when loading into the stack, it sits at the top of the stack and just presses into it. Heap allocation, on the other hand, requires finding a chunk of memory that is large enough to hold data before variables can be put in, Pointers can be generated (and, in many cases, Pointers can be pushed onto the stack for storage). Similarly, accessing stack variables is faster than accessing heap data.

What is ownership

Once we understand the difference between stack and heap, we can see that what we call GC is really about collecting memory on the heap. The ownership we’re talking about today is managing heap data, such as which parts of the code are using which data on the heap, minimizing duplicate data on the heap, cleaning up data that is no longer being used on the heap to make sure it doesn’t run out of space. All programming languages have their own way of managing computer memory. It can be broadly divided into two major schools:

  1. The language comes with a garbage collection mechanism that allows you to constantly look for unused memory while the program is running. Languages like JavaScript and Go have their own garbage collection mechanisms
  2. Languages do not have their own garbage collection, need developers to allocate and release memory, such as C, C++ is typical.

Rust, on the other hand, manages memory through an ownership system that does not slow down a program when it runs because the compiler checks the code according to a set of rules to determine when variables can be collected. The three most important rules of ownership systems are:

  • Each value in Rust has a variable called its owner
let a = 5   A is the owner of 5
Copy the code
  • Each value can have only one owner at any time
  • When owner leaves scope, the memory for this value is reclaimed

Third, the scope of variables

Rust allocates memory in a scope based on the scope management pointer, and releases it when it leaves the scope. The scope in Rust is also very simple. Rust is a lexical scope bounded by braces, with one curly bracket corresponding to one scope:

fn main() {
    let content = String::from("Srtian");
    println!("{}",content);
}
Copy the code

For example, the code above allocates memory for content at String::from, and after leaving the braces, content is freed from scope.

4. Specific performance of ownership

The previous sections have reduced Rust’s ownership system almost to a bare minimum, so let’s take a look at how ownership systems actually scope Rust’s memory management.

4.1. Transfer of ownership

In Rust, it is easy to copy a value of a known size to another value:

fn main() {
    let a =  "5" ; 
    let b = a ; // Copy the value a to b
    println!("{}", a)  / / 5
    println!("{}", b)  / / 5
} 
Copy the code

So a is stored in the stack, so we can copy it directly. However, for data placed in the heap, we cannot simply copy it (data placed in the heap, that is, managed by the ownership system) :

fn main() { 
	let s1 = String::from("hello");
	let s2 = s1; // copy s1 to s2
    println!("{}", s1)  // There will be an error because s1 has been freed here
    println!("{}", s2)  // hello
}
Copy the code

When we run the above code, we get an error. This is because when we copy the value stored in the heap, Rust, in order to prevent errors such as secondary release, handles this scenario by assuming that S1 is no longer valid. Rust then no longer needs to clean up S1 when it leaves scope. If you are familiar with languages such as JavaScript, you should be familiar with shallow copy and deep copy. In fact, this operation is shallow copy. It only copies Pointers, lengths, and sizes, not data directly. But Rust also invalidates the first variable directly, so it can’t be interpreted as a shallow copy.

One more note: Rust never automatically creates a “deep copy” of data. Therefore, all automatic replication can be considered to have little impact on runtime performance

When we do need to make a deep copy of the heap, we can use the Clone method to make a deep copy of the value:


#! [allow(unused)]
fn main() {
	let s1 = String::from("hello");
	let s2 = s1.clone();
	println!("s1 = {}, s2 = {}", s1, s2);
}
Copy the code

4.2 Borrowing of ownership

Ownership shifts or deep copies of variables are not sufficient for engineers’ daily development needs, such as:

fn main() {let contents = String::from("hello srtian");
    some_process(contents);
    println!("{}",contents); // error
}
fn some_process(word:String) {
    println!("some_process {}",word);
}
Copy the code

In the code above, the some_process(contents) contents variable “moves” to word, the parameter of some_process, and the contents variable cannot be used again. And many resources are too expensive in time and space for each memory reallocation, but there are many similar requirements in daily development. So Rust provides the option to borrow in this case. Borrowing ownership is also very simple, we just need to borrow the variable before the & character:

struct Person {
    age: u8
}

fn main() {
    let jake = Person { age: 18 };
    let srtian = &jake;

    println!("jake: {:? }\nsrtian: {:? }", jake, srtian);
}
Copy the code

In the above code, though, there is no Clone. But the above code will still compile and print. Similarly, if a non-replicable value is borrowed, it can be passed to the function as an argument, which solves the problem we mentioned above:

fn sum(vector: &Vec<i32- > >)i32 {
    let mut sum = 0;
    for item in vector {
        sum = sum + item
    }
    sum
}

fn main() {
    let v = vec![1.2.3];
    let v_ref = &v;
    let s = sum(v_ref);
    println!("sum of {:? }, {}", v_ref, s); // No error will be reported
}
Copy the code

However, it is important to note that we cannot change borrowed variables. This is actually consistent with the basic common sense of our daily life, borrowed things, we need to return the original. However, Rust also provides methods to make changes to borrowed variables:

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Copy the code

We need to change s to mut. Then the parameters passed in, as well as the parameters received, need to be explicitly identified as muT. Note, however, that there can only be one mutable reference for a particular data in a particular scope. This limitation allows variability, but in a limited way. The advantage of this is that Rust can avoid data contention at compile time. Data races are similar to race conditions and can result from three types of behavior:

  1. Two or more Pointers access unified data simultaneously
  2. At least one pointer is used to write data
  3. There is no mechanism for synchronous data access

Sometimes we want to return borrowed values. For example, if we wanted to return a longer string, we could write code like this:

fn longest(x: &str, y: &str) - > &str {
    if x.bytes().len() > y.bytes().len() {
        x
    } else {
        y
    }
}

fn main() {
    let jake = "jake";
    let srtian = "srtian";

    println!("{}", longest(jake, srtian));
}
Copy the code

The above code did not compile successfully and an error will be reported:

fn longest(x: &str, y: &str) - > &str {
                                ^ expected lifetime parameter
 
 = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
Copy the code

This is where the life cycle of a variable comes in, and the life cycle is the valid range of borrowed variables. Rust’s powerful compiler lets us implement them by inference in most cases without having to write them explicitly. However, in some scenarios that require lifecycle participation, we still need to manually add the declaration cycle function. For example, if we want to resolve the above error, we need to manually declare the lifecycle:

fn longest<'a>(x: &'a str, y: &'a str) - > &'a str {
    if x.bytes().len() > y.bytes().len() {
        x
    } else {
        y
    }

fn main() {
    let jake = "jake";
    let srtian = "srtian";

    println!("{}", longest(jake, srtian));
}
Copy the code

So we can return the borrowed variable.