Check out a free preview of the full The Rust Programming Language course

The "Memory" Lesson is part of the full, The Rust Programming Language course featured in this preview video. Here's what you'd learn in this lesson:

Richard discusses the concept of memory being interpreted as a u8 array or a u16 array, how the order of a struct effects the values in memory, and the resulting overhead of arrays, tuples, and stucts.


Transcript from the "Memory" Lesson

>> So, next, we're gonna talk about memory. And the reason we're gonna talk about memory is, basically, not because this is something that's really important to know about for this particular section, but rather because, we're going to start introducing the concept of memory as a way to build up to an understanding of Rust's Baro checker.

So, all the stuff we're gonna be talking about with regards to memory, it's kind of useful to know in terms of performance and just kind of how things work under the hood. But the main motivation here is that we're building up to something that will be helpful to you later on when we get to later sections about the Baro checker, and how to understand the error messages that it's giving you.

So, I know that people who are taking this course will have different backgrounds. I don't want to assume anyone has any computer science background or knows how memory works or anything like that. So, we're gonna start with a sort of very bare bones explanation of how this stuff works.

So, you can think of memory as a whole bunch of bits, 1s and 0s. So, a bit refers to, either, a 0 or a 1. And you can think of, conceptually, the memory in your computer is just a big long list of these 0s and 1s. Just like a lot of 0s, a lot of 1s, back to back.

Some of them are 0, some of them are 1. But 0s and 1s are not that useful. We wanna talk about things like strings and numbers that are bigger than 1. So, how do we get those? Well, the answer is that, basically, when we're looking at this, the program is interpreting sequences of 0s and 1s as something more interesting than that.

The smallest unit of this is, usually, what's called a byte, which is 8 bits. So, here we have the byte number 1 in this example memory that I've written out here, and it's 11110011. And as it happens, sorry, then the next 8 bits are byte 2 is gonna be 01101011.

And we've just decided that, this is how I'm gonna organize it. We're gonna have one byte, followed by another bite. Okay, sure, this is just a way of interpreting these bits. And you can imagine there's a third byte, and a fourth byte, and so on and so forth.

So, as it happens, 11110011, in binary, translates to 243 in decimal, which is the number that we're all familiar with. And these 1s and 0s translate 107. We're not gonna get into how to translate binary numbers into decimal numbers. You can, read up about that if you're curious about it.

But the point is that, in memory, actually what's there is a whole bunch of 1s and 0s, but, we choose to interpret that memory in different ways in order to get more useful values out of them. It's really just a way of choosing to interpret that. If you imagine, we could just interpret the first 16 bytes of something instead of separating the, I'm sorry, the first 16 bits, those 1s and 0s, as something rather than chopping them up into bytes like this.

All of those are totally fair options. Okay, so, I've sort of extended this out. So this one's 243, then the next eight bits are 107, then 117. I've basically written out, so here's some bytes in memory. Just remember that, it's backed by 1s and 0s. But, in this case, we're really only gonna be concerned with these bytes, here, in memory.

So, you can kinda think of this bytes representation of memory, as it's something like a u8 array, and like array of u8s. u8 being an 8-bit unsigned integer, namely a byte. So, this would be the first element in the array, the first byte, this is the next one, and so on and so forth.

And this is just a reasonable mental model to have of, essentially, how memory is represented in terms of just a long sequence of bytes. If you wanted to, though, you could also say, you know what? I wanna treat this as a u16 array. And then, the first element is here, which would actually be, if I took those same bits that we had before and interpreted them as a 16-bit unsigned integer.

That would be, instead of two different numbers of 243 and 107, those same 1s and 0s could be interpreted as 62,315, if we wanted to. Then, the next element in that u16 array, would be over here, and then and so on and so forth. So, basically, this is just an example of, we can look at the same 1s and 0s, and conceptualize them as different types of numbers, or strings, or, basically, anything we want to.

It's all 1s and 0s under the hood. And it's really a matter of just, how our program chooses to interpret those 1s and 0s, to do something useful. Okay, so, let's say that we have a u16 array that is exactly three elements long. So, we talked about arrays have to have a fixed length.

So, here we have u16; 3, and here are the three numbers in there. So, here's our first element. Here's our second element. Here's our third element. This is a perfectly reasonable thing that we could have in memory. Alternatively, we can also say, I wanna have a u32, which is even bigger.

So, this is like interpreting four bytes at a time, now. And this would be the first on this, and this would be the second one. But, for example, let's stick with just u16 and kind of go with that. So, here's another thing that's interesting about this, in memory, that three-element u16 array, has exactly the same memory representation as a tuple with three u16 sites, exactly the same.

There is absolutely no difference in the memory between these two. And you also notice that there's no overhead. So, in a lot of languages, particularly, object-oriented languages, when you have an object, let's say in memory, it's not just storing the data about the object. There's extra memory being used to store things like, what are the fields on this object?

So, you can change them at runtime if you want. Or metadata about, what's the class of this object? So, you can ask, at runtime, what's the class of this thing? Rust, really, as we talked about, optimizes for performance, so there was no overhead here. Literally for either a tuple or an array, it's just the numbers themselves in memory, in adjacent memory slots.

That is it, there is zero overhead to either of those. So, point.0, point.1, point.2, instead of (0), (1), (2), from a memory perspective, they're exactly the same thing, tuple versus array. And actually a struct, exactly the same thing too. Because, really, a struct is, essentially, just syntax sugar for a tuple, in Rust.

It's a way of getting something that works and, essentially, the same way, at runtime, as a tuple, except, you're specifying the value is in terms of labels, rather than in terms of slots. So, if we have this point here, we say, pt.x is this first slot, y is the second one, and z is the third one.

And this is why, when you're using structs in Rust, it always wants you to write out the word point when you're destructuring it or when you're creating one. Is because, it needs to know which order to put these in, in memory. Cuz this, actually, can have a very subtle effect on performance, sometimes.

It wants to know, hey, is x supposed to be the first one, or the second one, or the third one? I know you've got three of these things, but which order should they go in? And the order that you define them in the struct is actually how they're going to be laid out in the memory.

So, that's why the compiler asks you to write out the word point, is so that it knows how to lay these things out. Cuz, again, Rust wants to have as little overhead as possible. And, as we've seen, Rust tuples, structs, and arrays, they're all represented exactly the same way in memory, with exactly the same overhead, which is to say, no overhead, at all.

So, all three of these things, the tuple version, the struct version, and the array version, exactly the same overhead, exactly the same bytes in memory, same 0s and 1s. They're really, totally, indistinguishable to a running program. They just have different semantics depending on how you wanna use them and what's convenient for you in your code.

So, this is sort of our first taste of memory in Rust, and just getting a sense of how things work. But this is gonna be kind of an indication of, you can see how Rust is able to be faster than other languages. It's not that Rust has, necessarily, some sort of magical thing that lets it be faster than the hardware.

It's actually the opposite. It's that, Rust tries to give you these nice conveniences, at compile time, so, it feels like you have a lot of different features in the language. But, at runtime, it's basically saying, we want to have no overhead compared to if you were writing assembly language, as little overhead as possible.

We're not gonna store one single bit extra than is necessary to represent these numbers in memory. And that's kind of Rust, in a nutshell, and how it achieves the performance it does.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now