Skip to content

1.1 Your language already chose

This lesson does not talk about Rust yet. It assumes you already know another language—Java, Python, C++, Go, JavaScript, any of them—and that you have written some code in it. “Your language” below means that one. We are going to see a choice it made about memory before you noticed.

You have written b = a. After that line, both a and b still work, and you did not write a line that returns the memory behind a. It feels so natural that you take it for the way programming works.

That sentence is about two names pointing at one object: an object reference, a pointer, a slice. If your language copies the value instead, and each name gets its own memory, that is a different account. This lesson leaves that case alone. The figures and the table below cover only the shared case.

The natural feel was bought. Every language has to weigh three things at once: performance, safety, and expressiveness. You cannot take all three all the way. What a language favors, and what it leaves unpaid, is the feel of writing it. The language you use feels natural because it already made that choice.

This lesson sets the three out, then looks at why they pull against each other. The rules Rust uses do not appear yet.

Performance is how fast the code runs, and how much memory and CPU it uses. In this course it also includes when memory is freed: at a moment you can point to, or later, whenever the runtime gets around to a scan.

Safety here means memory safety: using memory that has already been freed, or never freeing a block. Type safety and concurrency safety count as safety too. The rest of this chapter stays with memory safety.

Expressiveness is how far the language lets you write down what you mean. It has two faces. The writing face is how high the abstractions sit, how short the syntax is, and whether both names still work after an assignment. The control face is whether you can work the memory address directly. The two faces are different things. A language can be strong on one and weak on the other.

The hard triangle: every vertex wants to be maxed out, yet each edge drags two of them down.
The hard triangle of performance, safety, and expressivenessA triangle whose three vertices are performance, safety, and expressiveness. The left edge joins safety and performance, labeled "where checks and reclaims happen"; the right edge joins performance and expressiveness, labeled "the cost of abstraction"; the bottom edge joins safety and expressiveness, labeled "how much power the programmer gets". The center reads "hard to max all three".hard to max all threesafety ↔ performancewhere checks & reclaims happenperformance ↔ expressivenessthe cost of abstractionsafety ↔ expressivenesshow much power the programmer getsPerformanceSafetyExpressiveness

A line you have heard makes the relation sound settled: fast, safe, easy—pick two. Fast is performance, safe is safety, and easy is the writing face of expressiveness. The line overreaches. Raise one, and you often have to lower another. The three tensions below are the three edges of the triangle.

The main ground of this edge is memory management, and it is where the course goes next. C leaves memory to the programmer. There is no check at run time, and the price is leaks and dangling pointers. Safety is given up. Java and Python reclaim memory with garbage collection. Safety is kept, but collection spends CPU, and the program can pause. The free happens on some later scan. You cannot point at the moment.

Garbage-collected languages: assignment copies the address, so two names point at one block of memory.
Two names share one block of memoryThe names a and b each hold an address; both arrows point at the same object on the heap. Reclamation happens later, when the runtime scans for names still pointing at it.namesabobject on the heapsame memory
C and C++ pointers: the free happens on the line you wrote. The other name still holds the old address.
Freeing is left to the programmerAfter the name a frees the object, the heap block is marked freed. The name b still holds the original address.namesa freesb still pointsmemory already returnedfreed

The two figures are the two ends of this edge. Both are about two names sharing one block of memory. In a garbage-collected language, b = a copies the address. Both names point at the same block, neither of them frees it, and reclamation waits for a later scan. Safety and the writing face of expressiveness are kept. A definite moment of freeing is given up. In C and C++, the free runs on the line you wrote, so the moment is definite. Free once too early, and the other name still points at memory that has been returned. Performance is kept. Safety is given up.

This edge is the cost of abstraction. A virtual function and dynamic dispatch let you organize code closer to the way you think about the problem. An interpreter skips the compile step, so the program runs as soon as you finish writing it. Those conveniences sit across a gap from the hardware instructions. A virtual call looks up a table before it knows which code to jump to. An interpreter translates each line before it runs. A compiler or an interpreter has to fill the gap. The stronger the writing face, the wider the gap tends to be, and the cost of filling it lands on performance.

Each end keeps one thing. Python and JavaScript keep the writing face. The code stays close to the way you would describe the work, and every run pays for the abstraction. C stays close to the hardware. There is little abstraction, the machine code is tight, and performance is kept. What is given up is the writing face of expressiveness: the same work takes more lines, and those lines sit closer to the machine.

Go is a third account. It cuts a good deal of the writing face so the syntax stays small and the concurrency model stays compact. That cut is not exchanged for a definite free. Go still uses garbage collection, so the moment of freeing is as uncertain as it is in Java or Python. What Go buys is a smaller language, not the performance defined in this lesson.

This edge is about how much power the language leaves with the person writing the code. That power is the control face of expressiveness. A pointer in C or C++ works the address directly. You can add to it, and you can aim it anywhere. The compiler cannot prove that it still lands inside a legal range. The more power you keep, the less the compiler can check for you.

So this edge is an exchange too. C and C++ leave the control face with the programmer. Control is kept, and memory safety is given up. An out-of-range access or a dangling pointer can show up only when the program runs. Java removes pointer arithmetic and takes that power away. Safety is kept. What is given up is control at the level of systems programming.

You cannot fill all three corners. Each language favors some of them and leaves others unpaid, and the unpaid part can be more than one. Two bills are the typical ones. Every assignment in the table is two names sharing one block of memory.

Garbage-collected languages (Python, JavaScript, Java, C#) C / C++ pointers
What it chose for you Assignment shares, and the runtime frees The free runs on the line you wrote
After assignment Both names work, and you write no free Both names work, and you decide who frees
When memory is freed A later scan, at an uncertain time The line you wrote, at a definite time
Favors Safety, and the writing face of expressiveness Performance, and low-level control
Leaves unpaid Performance (a definite time of freeing) Safety; the writing face of expressiveness
What you run into Pauses, and an uncertain time of freeing Dangling pointers, and leaks

Go does not sit in either column. Its free goes to the garbage collector, same as the left column, so the moment is uncertain, and that is also how it keeps safety. Its writing face is cut further back than the left column’s.

Neither bill is the complete one. They are two ways of spending the same triangle. What one column leaves unpaid is mostly what the other column favors. Spread the hard triangle out, and you get this table.

None of the three comes free. The language you use feels natural because it already chose what to favor and what to leave unpaid, and that choice is not written on the surface. Once you can see the unpaid part, every Rust rule you meet later answers the same question: which of the three is this rule taking back?

This lesson has no syntax to practice in the official Book. Chapters 1–3 are what you need before you start: let, fn main, println!, and cargo run. Leave chapter 4 for later.

Fill in the same bill for your language: what it favors in performance, safety, and expressiveness, what it leaves unpaid (that can be more than one item), and what you actually run into. The table covers only an assignment where two names share one block of memory. If you use Go, the time of freeing follows the garbage-collected column, and the writing face is cut further, as the section above describes. The next lesson picks up the first question Rust answers inside this triangle: when there is no garbage collector, who frees the heap?