Lesson 4.1 · 11 min
Where values live
Chatterbox's messages_seen is always 4 bytes. But line holds whatever you type, one word or a whole paragraph, and nobody told Rust how big that would be. Before the rules Rust is famous for, here is the picture every one of them is about: memory, the stack and the heap.
Adds to ChatterboxNothing new: a picture of the memory behind every value.
A question Chatterbox cannot dodge
Look at two names in Chatterbox's main as lesson 3.4 left it. messages_seen is a u32: 4 bytes (lesson 2.4), whether it holds 0 or four billion. line is a String holding whatever you typed: 3 bytes for hi and the Enter key (4 on Windows, where Enter adds two bytes), hundreds for a pasted paragraph.
The program was compiled long before you typed anything, so how did it make room for words it had never seen? A running program keeps values in two places, and one question decides which: is the size known before the program runs? Every rule in this chapter is about the second place.
Memory is a long row of numbered boxes
A computer's working memory is one enormous row of tiny boxes. Each box holds one byte, a number from 0 to 255, and has a number of its own, its position in the row, called its address. That is all memory is. A u32 is four boxes side by side; the letter h is one box holding 104.
A value that says where another value is, by holding its address, is a pointer. On a 64-bit computer, which is nearly every laptop and phone today, an address is 8 bytes. Keep that number in mind.
The stack: a pile of frames
Every time a function is called, it gets a frame: a small block of memory with one slot for each of its variables. Frames pile on top of each other, which is why this place is called the stack. When main calls reply, a frame for reply goes on top of main's; when reply returns, its frame comes off and every slot in it is gone at once. A frame is pushed, then popped.
That is fast: making room is moving one marker up, giving it back is moving it down. The condition is strict, though. The compiler plans each frame while compiling, so it must know every slot's size in advance. A u32 slot is 4 bytes, always. A slot that might need 3 bytes or 3,000, depending on what somebody types, cannot be planned.
Try to break that rule. [0; 5] is an array of five zeros (arrays are from lesson 3.3), and an array sits right inside the frame. What if its length comes from text the program has just measured?
Does this compile — and if it does, does it run?
- It compiles and runs
- Compile error E0435
- It compiles, then panics
Reveal the answer
It does not compile: E0435. An array's length is part of its size, and the frame is planned before the program runs. size is a let, worked out only while running (it would be 5, but the compiler does not run your code to find out), so the compiler refuses with E0435, "attempt to use a non-constant value in a constant". When a size truly is fixed, a const (lesson 2.6) works, the way MAX_LEN does. When it depends on what somebody types, no frame can hold it, and the value needs the second place.
error[E0435]: attempt to use a non-constant value in a constant --> src/main.rs:4:22 | 4 | let buffer = [0; size]; | ^^^^ non-constant value | help: consider using `const` instead of `let` | 3 - let size = typed.len(); 3 + const size: /* Type */ = typed.len(); |
The heap: room asked for while running
The second place is the heap: a large area of memory a program can ask for pieces of while it runs, of whatever size it needs right then. Asking is called allocating: the allocator finds a free block of the right size and hands back its address, and when the program has finished with the block, it hands it back for reuse.
That costs more than the stack, because finding a block takes work and every block must be handed back exactly once. In return the heap can hold something whose size is only known at run time, and that may grow later.
A String uses both places. Its text lives in a heap block. In the frame sits a fixed part of three numbers: a pointer to the text's first byte, the length (bytes of text now, what len() reports) and the capacity (bytes the block has room for). Three times 8 bytes is 24 bytes, whether the text is empty or a novel. So line got a slot in main's frame although nobody knew what you would type: the slot holds directions to the text, not the text.
A &str (lesson 2.5) is the same idea with one number fewer: a pointer and a length, 16 bytes, looking at text that lives elsewhere, in a String's heap block or inside the program for quoted text. It never grows, so it needs no capacity.
sizes measured with size_of on this compiler (64-bit), not quoted
| Type | Bytes | Layout |
|---|---|---|
String | 24 | pointer (8 B) · length (8 B) · capacity (8 B) → heap: the text itself |
&str | 16 | pointer (8 B) · length (8 B) |
u32 | 4 | the number (4 B) |
bool | 1 | true or false (1 B) |
You can measure these yourself with size_of, a function from Rust's standard library that reports how many bytes a type takes in a frame. Its question is about a type, not a value, so the type is not passed in the round brackets (they stay empty). It goes in angle brackets straight after the name, joined by ::: size_of::<String>() reads as "size_of, for String". The use std::mem::size_of; line brings the function into reach, the way use std::io; did in lesson 3.4. Guess what this prints.
What does this print?
14 2414 1424 1414 38
Reveal the answer
The program prints: 14 24 len() counts the text in the heap block: 14 bytes. size_of::<String>() measures the fixed part in the frame: 24, for every String there is. Different questions, different answers.
Watch a program use both
Step through a small program. On the right, frames stack up from main's, with heap blocks beside them. A solid arrow from a slot to a block is a String's pointer.
Step through the program. Solid arrows are ownership; dashed arrows are borrows. Use ← → or the slider.
- Line 6. The program starts, and main gets the first frame on the stack. The compiler already knows its size, because it knows the size of every variable in it.
- Line 7. messages_seen is a u32: its 4 bytes sit right in main's frame, holding 4. Nothing on the heap.
- Line 8. String::from("hi") asks the allocator for a 2-byte heap block and copies the text in. line's 24 bytes in the frame hold the block's address, the length 2 and the capacity 2.
- Line 9. The text no longer fits in 2 bytes. The String asks for a bigger block (here 8 bytes), the old text is carried across, and the old block is handed back. (An allocator can sometimes stretch a block where it is, but a String cannot count on it.) Only the three numbers in the frame changed; line's slot is still 24 bytes.
linenow owns heap blockh2. - Line 10. Calling is_busy pushes a new frame on top of main's. Its parameter count gets a copy of the 4 bytes of messages_seen.
- Line 2. limit is another 4-byte slot, in is_busy's frame.
- Line 3. count > limit is 4 > 3, which is true. That bool is the function's answer, because it is the last expression.
- Line 10. is_busy returns and its frame is popped: count and limit vanish in one go, with nothing to hand back, because nothing in that frame pointed at the heap. The answer lands in main's frame as busy, one byte.
- Line 11. messages_seen changes in place, in its own slot: now 5.
- Line 12. This prints hi there / 5 / true. The text is read by following line's pointer to the heap block. stdout:
hi there / 5 / true - Line 13. main ends and its frame is popped. line was the variable responsible for the heap block, so the block is handed back right here, exactly once. Nothing is left over.
Notice the difference. Frames were made and thrown away whole, for free. The heap block had to be asked for, asked for again when the text grew, and handed back at the end. That last part must happen exactly once: never, and the memory is lost (a leak); twice, and the program can crash. Here it went back because line was responsible for it. How Rust decides who is responsible is the next lesson.
Chatterbox's own memory
Here is main from Chatterbox v0.3, after you typed Sam as your name and hello as a message.
| Name in main | Type | Bytes in the frame | On the heap |
|---|---|---|---|
messages_seen | u32 | 4 | nothing |
quiet | bool | 1 | nothing |
name_line | String | 24 | 4 bytes of text, Sam and the Enter key, in a block that is probably bigger: a String that grows asks for at least 8 bytes at a time, leaving spare room |
name | &str | 16 | nothing of its own: it points at the Sam inside name_line's block |
line | String | 24 | the line being read: 6 bytes of text, hello and the Enter key, in a block with room to spare |
bytes | usize | 8 | nothing |
message | &str | 16 | nothing of its own: it points at the hello inside line's block |
BOT, MAX_LEN, HELP | constants | none | none: a constant is written into the program wherever it is used, and quoted text is stored inside the program itself |
Two things in code you already have make sense now.
- Why
lineis made once, before the loop.line.clear()sets the length to 0 but keeps the heap block. So every line you type is read into the same block, and the allocator is asked again only when a line is longer than any before. A new String inside the loop would ask for a block and hand it back every turn. - Why
trim()costs nothing.nameandmessageare&strs that look at part ofname_line's andline's text, skipping whattrimcut off. No text is copied.
Which raises a question. message points into line's heap block. What if that block were cleared or handed back while message still pointed there? In many languages that is a bug you find months later. In Rust it does not compile, and the next five lessons are why.
A warm-up, separate from Chatterbox. Write underline: it returns the title, then a newline, then one = for every byte of the title. Build the answer in the String that starts empty. While your loop runs, its 24 bytes in the frame stay exactly where they are, and only its heap block grows.
A warm-up, separate from Chatterbox. Write underline: it returns the title, then a newline, then one = for every byte of the title. Build the answer in the String that starts empty. While your loop runs, its 24 bytes in the frame stay exactly where they are, and only its heap block grows.
Hidden tests (4): counts_bytes_not_letters an_empty_title_is_just_a_newline the_whole_text_has_the_right_length underlines_the_name
This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.
Say it out loud, then reveal
What decides whether a value can live entirely in a stack frame?Reveal
u32 always is; text somebody types is not.What are the 24 bytes of a String?Reveal
What happens to a function's variables when it returns?Reveal
Why does Chatterbox make line once, before its loop?Reveal
clear() empties the text but keeps the heap block, so every turn reuses the same block instead of asking for a new one.Your Chatterbox so far: v0.3 (after lesson 3.4)
Download this version (zip)Unzip, then cargo run inside the folder.
That is lesson 4.1. Tick it off so the course remembers where you are.
Go deeper
Questions people ask
What is the difference between the stack and the heap in Rust?
The stack holds each function call's frame, with a slot of known size for every variable, and frames are thrown away whole when functions return. The heap holds blocks asked for while the program runs, for things like text whose size is not known in advance, and each block must be handed back exactly once.
Why is a String 24 bytes in Rust?
The part of a String that sits in a frame is three numbers of 8 bytes each on a 64-bit computer: a pointer to the text on the heap, the length of the text and the capacity of the heap block. The text itself is not counted, because it lives in the heap block.
How big is a &str in Rust?
On a 64-bit computer a &str is 16 bytes: a pointer to the first byte of the text and a length. It has no capacity, because a &str only looks at text and can never grow it.
Is the heap slower than the stack?
Asking for a heap block and handing it back takes more work than pushing and popping a frame, which is little more than moving a marker. That is why Chatterbox reuses one String for every line it reads instead of making a new one each turn.
