notebook № 097 plates · 16 min to read, longer to play
Who frees the memory?
On giving things back
Every program borrows memory and has to give it back. The interesting part is who decides when. This notebook walks through the main answers languages have found, from doing it by hand to letting a collector sweep up, from counting references to proving ownership at compile time, and ends with a table of which one fits what.
plate I · by hand
You free it yourself
A running program keeps its short-lived values on the stack, which tidies itself up whenever a function returns. Anything that has to outlive the function, or whose size isn’t known in advance, goes on the heap: a big shared pool where the program asks for a block and, sooner or later, has to hand it back.
The oldest answer to “who hands it back?” is you do. In C you call malloc to get a block and free to return it. Nothing checks your work. That makes it fast and completely predictable, and it makes three classic bugs possible.
Forget to free and the program leaks: it slowly eats memory until something gives. Free too early and you get a use-after-free: the old pointer now reads whatever moved in next. Free twice and the allocator’s own bookkeeping gets corrupted. Use-after-free alone is behind a large share of serious security holes in big C and C++ codebases.
who uses it C, Zig, and C++ under its smart pointers. Most operating systems, browsers, databases and game engines are built on it.
Run “free too early” to the end. Where did the PIN come from?
plate II · tracing
The janitor walks the heap
A tracing garbage collector flips the question round. The program just allocates and never frees. Every so often the collector starts from the roots (local variables, globals), follows every pointer it can reach and marks what it finds. Whatever it didn’t reach can’t be used again, so it’s swept away. That’s mark and sweep.
Leaks of the forgotten-free kind disappear, and so do use-after-free and double frees. Cycles are no problem either: two objects pointing at each other but at nothing reachable simply don’t get marked.
The price is paid in two currencies. Memory: a collector needs room to let garbage pile up between collections. And time: marking costs work in proportion to what’s alive, and simple collectors stop the program while they do it. Modern ones (Go’s, Java’s G1 and ZGC, .NET’s, V8’s in Chrome) do most of the work alongside the program and split the heap into young and old generations. ZGC advertises pauses under a millisecond. You still pay in CPU, you just pay in small coins.
who uses it Java and Kotlin, C#, Go, JavaScript, Ruby, OCaml, Haskell, Dart and most Lisps.
Slide headroom down to 1.2× and watch the red stripes. Then try 3×.
plate III · counting
Everyone keeps a tally
Reference counting takes the opposite approach. Every heap object carries a number: how many references point at it. Copy a reference and the number goes up. Drop one and it goes down. At zero, the object is freed on the spot, no collector, no pause, and you know exactly when it happens.
That promptness is why Apple chose it. Swift and Objective-C use automatic reference counting (ARC): the compiler writes the increments and decrements for you. CPython counts too, which is why a file you stop using in Python usually closes right away.
Two catches. Every copy of a reference costs a write to memory, and atomic writes if threads share the object. And a cycle is invisible to it: a person who points at a flat whose tenant points back at the person keeps both counts at one forever. Swift asks you to mark one side weak. Python and PHP run a separate cycle collector now and then, just to find them.
who uses it Swift and Objective-C (ARC), Python, PHP, Perl, C++ shared_ptr, Rust’s Rc and Arc.
Run “a cycle”, then “a cycle, one side weak”. What changed when the last line ran?
plate IV · ownership
One owner, many borrowers
Rust asked whether the compiler could do C’s bookkeeping, correctly, before the program ever runs. Its answer is ownership. Every value has exactly one owner, and when the owner goes out of scope the value is freed, right there, with a call the compiler puts in for you.
Hand a value to someone else and ownership moves: the old name is dead, and using it is a compile error, not a crash at 3 a.m. To let code look at a value without taking it, you lend it out as a borrow, &s. The borrow checker enforces one rule: many readers or one writer, never both, and no borrow outlives the value.
The reward is C’s speed and predictability without its three bugs, and safe sharing between threads too. The cost lands on the programmer: some perfectly good designs, graphs and doubly linked lists especially, have to be reshaped to please the checker, or reach for Rc and RefCell, which bring counting back.
who uses it Rust. C++’s unique_ptr and RAII are the unchecked ancestor; Swift and Mojo have since added ownership features of their own.
Try “use after move”, then “two writers”. Both are caught before anything runs.
plate V · compile-time counting
Counting at compile time
Lobster, a language for writing games, asked a gentler question than Rust: what if we keep reference counting, but let the compiler skip every count it can prove is pointless? It calls the result compile-time reference counting.
While it type-checks, the compiler picks one owner for each new allocation and treats every other use as a borrow, much like Rust. Borrows need no counting at all. The difference is what happens when the analysis can’t make it work: where Rust stops with an error, Lobster quietly inserts a single count update for that one use and carries on.
Its author reports that this removes around 95% of the runtime count operations. The rest stay, so memory is still freed the moment the last owner lets go, with no collector and no pauses, which is what a game running at 60 frames a second wants. Cycles still leak, so Lobster prints a cycle report when the program exits, telling you where to break them.
Miri, the GPU-first language I’m building, belongs to this family, even though its compiler calls the pass Perceus. What it actually runs is non-atomic counts on heap blocks. Parameters are lent by default, and a whole-program pass infers which ones must be owned. Locals borrow out of those owned roots, and out is the only way to get a mutable reference. It does this on its mid-level IR rather than in the type checker, which keeps the compiler’s verifiers in play, and on the workloads where it matters it removes about as many count updates as Lobster reports.
What Miri adds is the part that matters most for buffers: structs and collections have copy-on-write value semantics. Assignment shares the memory, the first write to a shared value copies it, and the uniqueness checks behind that are hoisted or deleted by later passes. As for cycles, the compiler refuses the most direct kind, an object stored into its own field.
who uses it Lobster, and Miri with copy-on-write values on top. The same goal, counting less by proving more, drives Swift’s ARC optimiser and the Perceus family of plate VI.
Turn on the analysis, then add “keep a target list”. One +1 comes back. Why that one?
plate VI · perceus
Counting precisely, reusing in place
Perceus, from Microsoft Research and first built for the Koka language, makes reference counting precise. The compiler writes every count update itself and drops each reference straight after its last use, not at the end of the scope. The paper calls this garbage free: at any moment, only memory that will still be used is kept.
Then comes the trick. Functional code never changes data; it builds new data from old. Mapping over a list allocates a new cell for every old one, and the old ones die. But if an old cell’s count is one at the moment it dies, nobody else can see it, so its memory is reused for the new cell, in place. Purely functional code gets the speed of mutation. The authors call it functional but in-place.
Lean 4, the proof assistant and language, uses the same family of techniques (its creator co-wrote the Perceus paper). Cycles stay reference counting’s blind spot here too: Perceus frees everything else promptly, and leaves cycles to the programmer.
Miri’s compiler names its counting pass after Perceus, but what it runs is closer to Lobster’s ownership inference, with copy-on-write values on top: that’s in plate V.
who uses it Koka and Lean 4.
Run all three modes, then tick “list is shared” and run “+ reuse” again.
map(xs, x => x + 1) over five cells. The code is map with the counting each mode inserts, in a simplified form of the Perceus paper’s notation; the lit lines are the path taken.plate VII · the table
Which one, for what
Line them up and the pattern is plain: each model chooses who pays, and when. Manual memory charges the programmer. A tracing collector charges memory and a little time, in batches. Reference counting charges a small fee on every copy. Ownership charges the programmer once, at compile time. Lobster and Perceus try to keep counting’s promptness while sending most of its bill to the compiler.
| Model | Who uses it | Frees memory | You write | Run-time cost | Pauses | Cycles | Safety |
|---|---|---|---|---|---|---|---|
| Manual | C, Zig, C++ (raw) | When you call free | Every free, at the right moment | None beyond the allocator | None | Your job | Leaks, use-after-free, double free |
| Tracing GC | Java, C#, Go, JavaScript, Ruby, Haskell | Later, in batches, when the collector runs | Nothing | Marking work, write barriers on pointer stores, 1.5–3× memory headroom | Short and concurrent in modern collectors | Collected | Memory-safe |
| Reference counting | Swift, Objective-C, Python, PHP, shared_ptr | The moment the count hits zero | weak on back-pointers | A count update on every copy; atomic if shared across threads | None, but a big structure can cascade | Leak, unless weak refs or a cycle collector | Memory-safe |
| Ownership & borrowing | Rust | When the owner goes out of scope | Code the borrow checker accepts | None | None | Hard to build; leak with Rc | Proven at compile time, threads included |
| Compile-time RC | Lobster, Miri (with copy-on-write values) | The moment the last owner lets go | Nothing, almost always | Lobster reports about 5% of plain RC’s count updates remain | None | Leak; Lobster reports them at exit, Miri refuses direct self-cycles | Memory-safe |
| Perceus | Koka, Lean 4 | Right after the last use | Nothing | Precise counts; unique data reused in place | None | Leak | Memory-safe |
| Model | Web & backend servicesmemory-safe · quick to write · leaks unlikely | Games & real-timeno pauses · low CPU overhead · quick to write | Systems & embeddedno collector · no hidden cost per copy · memory-safe | Data & GPU pipelineshigh throughput · buffers freed promptly · quick to write | Best for |
|---|---|---|---|---|---|
| Manual | 0 of 3misses: memory-safe, quick to write, leaks unlikely | 2 of 3misses: quick to write | 2 of 3misses: memory-safe | 2 of 3misses: quick to write | Kernels, allocators and drivers, where nothing else may run |
| Tracing GC | 3 of 3has all three | 1 of 3misses: no pauses, low CPU overhead | 1 of 3misses: no collector, no hidden cost per copy | 2 of 3misses: buffers freed promptly | Services and apps where shipping fast beats exact timing |
| Reference counting | 2 of 3misses: leaks unlikely | 2 of 3misses: low CPU overhead | 2 of 3misses: no hidden cost per copy | 2 of 3misses: high throughput | UI apps and anything holding files, sockets or GPU handles |
| Ownership & borrowing | 2 of 3misses: quick to write | 2 of 3misses: quick to write | 3 of 3has all three | 2 of 3misses: quick to write | Systems software that must be fast and safe at once |
| Compile-time RC | 2 of 3misses: leaks unlikely | 3 of 3has all three | 2 of 3misses: no hidden cost per copy | 3 of 3has all three | Game code that wants easy memory without frame hitches |
| Perceus | 2 of 3misses: leaks unlikely | 3 of 3has all three | 2 of 3misses: no hidden cost per copy | 3 of 3has all three | Functional and data-transform code that should run like imperative code |
The lines blur in practice. Python counts and traces. Rust programs reach for Rc when a graph needs several owners. C++ mixes manual memory, RAII and shared_ptr in one codebase. Even the theory says they’re relatives: a 2004 paper showed tracing and counting are two ends of one spectrum, and every real collector sits somewhere in between.
If you remember one thing, make it this: memory is always freed by someone, at some time, for some price. The interesting choice is which of the three you’d rather fix. The compiler notebook shows where in the pipeline passes like Perceus do their work.
Switch to “where it fits” and pick the column closest to what you build. Which row would you have guessed?
Questions people ask
- What is the difference between garbage collection and reference counting?
A tracing garbage collector runs now and then, follows every pointer from the program’s roots and frees whatever it didn’t reach, so it handles cycles but needs spare memory and some pause time. Reference counting keeps a count on every object and frees it the moment the count hits zero: prompt and predictable, but it pays on every copy and leaks cycles unless something else catches them.
- What is Perceus reference counting?
Perceus is a reference-counting scheme from Microsoft Research, first built for the Koka language. The compiler inserts the count updates itself and drops each reference right after its last use, so no garbage is kept around. When an object’s count is one at the moment it dies, its memory is reused in place for the new value, which lets purely functional code update data in place.
- Does Rust have a garbage collector?
No. Rust’s compiler gives every value one owner and frees it when the owner goes out of scope, and the borrow checker proves at compile time that no reference outlives the value. For data that really needs several owners, the standard library offers reference-counted pointers,
RcandArc, which the programmer picks explicitly.
Related material
elsewhere in the notebooks
further reading
- Perceus: Garbage Free Reference Counting with Reuse Reinking, Xie, de Moura & Leijen, PLDI 2021 The paper behind plate VI: precise drops, reuse analysis and “functional but in-place” code.
- Memory Management in Lobster Wouter van Oortmerssen How Lobster infers owners and borrows, and why that removes most of its reference counting.
- What Is Ownership? The Rust Programming Language The friendliest introduction to moves, borrows and drops, straight from the Rust book.
- A Unified Theory of Garbage Collection Bacon, Cheng & Rajan, OOPSLA 2004 Tracing and reference counting turn out to be mirror images, and real collectors are blends of both.
- A Guide to the Go Garbage Collector The Go team The memory-for-CPU trade-off of plate II, with the real knob (GOGC) and real formulas.
- Miri’s memory model miri-lang.org Non-atomic counting with whole-program ownership inference, borrowed locals and copy-on-write values.