Post

4. From V8 to LLVM: Differences in Memory Management

4. From V8 to LLVM: Differences in Memory Management

Since we’re already familiar with V8’s behavior, the event loop, and TypeScript’s structural type system, we’ll skip programming basics and go straight to Rust’s core design. We’ll use The Rust Programming Language to move from “runtime interpretation” to “compile-time constraints.”

1. Core paradigm shift: reshaping the worldview

In TypeScript, your world is guarded by GC (garbage collector). We create objects, pass references; V8 quietly does reference counting or mark-and-sweep. We focus on “business logic,” not “memory lifetime.”

Rust’s world has no GC. But it’s not C++-style manual free either. Rust introduces a third model: ownership and borrowing.

You can think of the Rust compiler (on top of LLVM) as an extremely strict, non-configurable ESLint that enforces memory-safety rules at compile time. If the code compiles, it’s usually memory-safe.

2. Memory model: what a “variable” really is

In TS, const a = { val: 1 }; — the variable a is just a reference (pointer) to that object on the heap.

In Rust, types split into two: those that implement Copy (stack data) and those that don’t (they own heap resources).

2.1 Binding & ownership

Rust’s core rule: at any time, a value has exactly one owner. This applies when ownership is transferred (Move). For basic types, Rust behaves like TS primitives.

Case A: Stack data (primitives) — Copy

For simple types with known size at compile time (integers, floats, bool, char, and tuples/arrays of only these), Rust does Copy automatically. This matches TS number behavior.

  • TypeScript (value copy):
1
2
3
let x = 5;
let y = x; // y is a copy of x
// x is still valid and independent
  • Rust (automatic Copy):
1
2
3
4
5
6
7
8
let x = 5;
let y = x;
// ✅ Copy happens!
// Because i32 implements the Copy trait.
// They live entirely on the stack; copying is very fast; we don't need move semantics to protect heap memory.

println!("x = {}, y = {}", x, y); // Compiles; x is still valid

Case B: Heap data (complex types) — Move

This is the biggest difference from TS. For types that own heap memory like String, Vec:

  • TypeScript (shared reference):
1
2
3
4
let a = { name: "TS" };
let b = a;
// a and b point to the same heap address.
// Changing b.name affects a. V8 GC decides when to collect.
  • Rust (ownership transfer):
1
2
3
4
5
6
7
8
9
let a = String::from("Rust");
let b = a;
// ⚠️ Move happens!
// 1. The stack data (ptr, len, cap) is copied to b.
// 2. The heap data is NOT copied (no deep copy).
// 3. The compiler declares: a is invalid.

// println!("{}", a); // Compile error: use of moved value: `a`

Why must a lose ownership? If both a and b were valid, when they go out of scope Rust would call drop() for each. That would free the same heap block twice (double free), a serious memory bug.

Summary for senior TS developers:

  • Copy semantics: Like TS primitive types (number, boolean). In Rust: i32, bool, f64, char, and tuples/arrays of only these. Data lives on the stack.
  • Move semantics: Like TS object types (Object, Array), but Rust forbids two variables both having “write rights” or “free rights” to the same object, so the old variable is invalidated.

2.2 Precise stack vs heap control

In TypeScript (V8), an array is a black box. It might be packed small integers or holey elements; V8 adjusts storage under the hood.

Rust forces you to decide memory layout when you define the data structure, in exchange for predictable performance.

ConceptTypeScriptRustMemory (default)Notes
Tuple[number, string](i32, String)Stack (mixed)Fields are laid out on the stack. If the tuple contains String, the String metadata is on the stack; the actual text is on the heap.
ArrayFixedLengthArray (TS type gymnastics)[i32; 5]StackFixed at compile time. In memory it’s just 5 consecutive integers, no header. No push/pop.
VectorArray<T>Vec<T>Heap (data)Grows. Stack holds a “fat pointer”; data is on the heap.

Mental model for TS developers:

  1. Array [T; N] is “inline”: e.g. struct Point { coords: [f32; 3] } — those 3 floats are embedded in the struct. No pointer indirection, no cache misses. Big win for math or graphics.
  2. Vector Vec<T> is a fat pointer: On the stack it’s not just a pointer; it’s a struct with three fields (typically 24 bytes on 64-bit):
    • ptr: pointer to the start of the heap allocation
    • len: current length
    • capacity: total allocated capacity

V8 comparison: When you push on a TS array, V8 might switch internal storage from a C++ array to a hash map if it gets sparse. Rust’s Vec is always contiguous; when capacity is exceeded it allocates a larger block, memcopies, and updates ptr.

When to use what:

  • [T; N]: Fixed-size buffers, math vectors (e.g. Vector3), fixed-size hashes (e.g. SHA-256 result).
  • Vec<T>: User input, file contents, any collection whose size is known only at runtime.

3. Variables & mutability: immutable by default

3.1 Deep immutability

  • TS: const only locks the reference. const obj = {}; obj.x = 1 is valid.
  • Rust: let locks everything by default.
1
2
3
4
5
6
let x = 5;
x = 6; // Compile error: default is immutable

let mut y = 5; // Must explicitly declare mutability
y = 6; // OK

Compiler view: Without mut, the compiler can do aggressive constant folding and register optimization, because it knows that memory won’t change.

3.2 Shadowing

TS developers may find reusing variable names odd. Rust encourages it, mainly for type conversion chains.

1
2
3
let spaces = "   ";           // Type: &str
let spaces = spaces.len();    // Type: usize (shadows the previous variable)

Under the hood these are two different stack slots. For the compiler they’re two different variables that happen to share a name, which avoids names like spaces_str and spaces_num.

The main challenge from TypeScript to Rust isn’t syntax (that’s easy) but mental model:

  1. Always think about memory: Is this on the stack or heap? Who owns it?
  2. Work with the compiler: The TS compiler is your advisor; the Rust compiler is your guard. Don’t try to bypass it; understand why it errors. Usually it’s pointing at a real memory-safety issue.
  3. Start from the big picture: Use what you know about V8 to think about how Rust achieves similar automatic memory management without GC (e.g. RAII).

Suggested path: Start with The Rust Programming Language, and for each new concept, draw a “memory layout” diagram. Once you see how pointers flow on stack and heap, Rust becomes much clearer.

This post is licensed under CC BY 4.0 by the author.

Trending Tags