Skip to content

1.1 The hard triangle: what does automatic memory reclamation cost?

Before learning a new programming language, start with a basic question: when the world already has C and C++, as well as Java, Python, and Go, why invent Rust?

The answer lies in its primary design goal: getting both hardware-level performance and memory safety at the same time. For decades, those two goals stood in a tug-of-war. When you write b = a for an object in Python, Java, or JavaScript, both variables point to the same object in memory, you never write cleanup code, and the program runs safely—because the language shifts the cost of memory management onto the runtime. In C and C++, code runs right on the hardware, but the programmer has to write deallocation code by hand and guard against memory bugs personally.

Why didn’t earlier languages simply combine high performance with strong safety? To understand what Rust is aiming for—and what price it pays to get there—we need to step back to the underlying trade-offs of programming language design and look at three competing dimensions:

  • Performance: how little overhead your code pays on the hardware.
  • Safety: whether the program prevents memory bugs such as reading freed memory or stepping past valid bounds.
  • Expressiveness: both the power of high-level abstractions and how freely you can write code without restrictions.

We call the tension among these three the hard triangle of programming languages. It is a spectrum, not a binary choice—lean hard into any two, and you pay more on the third. This section follows the first edge Rust has to conquer: safety versus performance, which comes down to who frees memory and when.

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 "who frees, and when"; 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 ↔ performancewho frees, and whenperformance ↔ expressivenessthe cost of abstractionsafety ↔ expressivenesshow much power the programmer getsPerformanceSafetyExpressiveness

Safety and performance: when memory is freed

Section titled “Safety and performance: when memory is freed”

The split along this edge is simple: when does a block of memory get freed, and who makes that call? Mainstream languages take one of two paths:

  • Leave it to the programmer (manual management in C / C++). Memory is freed on the exact line where you write free or delete, with no runtime scanning or counting overhead. You pay for that speed in safety—read a pointer after freeing it, or free the same block twice, and you get a memory bug. C++ adds automatic cleanup through RAII and smart pointers, but the compiler still does not generally check whether other pointers keep reading freed data.
  • Leave it to the runtime (Java / JavaScript / Python). A runtime collector finds unreachable memory and reclaims it for you. Java, JavaScript, and Go use tracing garbage collection (tracing GC), scanning the heap in the background; collection timing is nondeterministic, which can introduce pauses or tail-latency spikes under heavy concurrency. CPython—the standard Python implementation—relies mainly on reference counting, freeing most objects the moment their count hits zero while falling back to a generational collector for reference cycles.

Side by side, the two approaches look like this:

Dimension Runtime reclamation (Java / JavaScript / Python / Go) Manual deallocation (C / C++)
When memory is freed Nondeterministic with tracing GC; mostly immediate with CPython reference counting On the exact line where free or delete executes
What it favors Safety and unrestricted sharing—share objects freely without planning cleanup Raw performance
What it costs Performance: CPU and memory spent scanning or counting, GC pauses, latency spikes Safety: the programmer must manually prevent use-after-free, double frees, and leaks
Typical production failures Reduced throughput, stop-the-world pauses, bloated memory footprint Hard-to-trace crashes, data corruption, or slow memory leaks

Performance and expressiveness: does an abstraction add work at runtime? When Python evaluates a + b, the runtime inspects both object types before deciding which addition routine to run; in C, adding two int values compiles straight down to a single machine instruction. High-level abstractions don’t always cost extra at runtime—it depends on how much work the compiler can finish ahead of time.

Safety and expressiveness: how much raw power does the programmer get? The more freely you can manipulate raw addresses, the less the compiler can verify. C lets you cast any integer into a pointer and write to it, leaving the compiler no way to know whether that address is valid. Java removes pointer arithmetic altogether and checks array bounds at runtime, throwing an exception on an invalid index.

We won’t pursue those two edges here.


Rust’s breakthrough: automatic reclamation without GC

Section titled “Rust’s breakthrough: automatic reclamation without GC”

The two classic camps force a binary choice: accept GC overhead for safety, or write free by hand for speed and shoulder the risk yourself.

Cross “who decides when to free” with “whether a runtime collector is involved,” and four quadrants appear. Only the bottom-left quadrant gives you automatic cleanup without a runtime collector—and that is the exact spot Rust is after.

Four quadrants: who frees, and whether a runtime collector is involvedA two-by-two quadrant chart. The top row is "the programmer decides when to free"; the bottom row is "automatic reclamation". The left column is "no GC, no runtime collector"; the right column is "GC, runtime reclamation". Top left lists C free and hand-written delete in C++. Top right is marked "—", an impossible combination. Bottom left reads: the approach Rust seeks, automatic reclamation without GC. Bottom right lists tracing garbage collection and reference counting.No GCno runtime collectorGCruntime reclamationProgrammerdecidesAutomaticC freehand-written delete in C++—impossibleThe approach Rust seeksautomatic reclamation · no GCTracing garbage collectionreference counting

To keep both safety and performance without compromise, only the bottom-left quadrant remains: memory must be reclaimed automatically (not by hand), yet without computing when to free it at runtime (no GC).

Follow that thought for a second and it sounds like a paradox. If the programmer never writes free, and there is no garbage collector watching objects at runtime, how can the program know when a piece of memory is no longer needed—or which line should free it?

Because that puzzle looks almost unsolvable at first glance, mainstream language design treated the bottom-left quadrant as a no-go zone for decades, assuming that “automatic memory management” automatically meant “use a garbage collector.”

Rust’s entire ambition is to walk straight into that no-go zone and make it work. Of course, the hard triangle still collects its due: when neither safety nor performance gives an inch, the price lands on how freely you can write code.

How can a language figure out when to free memory without relying on either the programmer or a runtime collector? That question drives every section that follows. Next, we look at the two regions of memory a program uses—and see which one actually causes all the trouble.