V8pedia

Pointer compression

On 64-bit builds, V8 stores most heap pointers as 32-bit values, not 64-bit. This roughly halves the size of pointer-heavy objects, packs more of the heap into cache, and reduces GC work — and the decode cost is a single add. It is one of V8's highest-leverage memory optimizations, and a great case study in exploiting an invariant (the whole heap fits in 4 GB) for a big win.

::: info Ubiquitous language Cage: the contiguous virtual-address region the V8 heap lives in. Cage base: its start address. A compressed pointer (Tagged_t) is the 32-bit offset of an object from the cage base. :::

The idea

A 64-bit pointer is wasteful when your entire heap fits in 4 GB: the high 32 bits are nearly constant. So V8 reserves a 4 GB-aligned, 4 GB-sized region — the cage — and stores pointers as 32-bit offsets within it:

#ifdef V8_COMPRESS_POINTERS
constexpr size_t kPtrComprCageReservationSize = size_t{1} << 32;   // 4 GB
constexpr size_t kPtrComprCageBaseAlignment   = size_t{1} << 32;   // 4 GB-aligned
const int kApiTaggedSize = kApiInt32Size;   // tagged values are 32-bit
#else
const int kApiTaggedSize = kApiSystemPointerSize;
#endif

include/v8-internal.h#L164-L180

Because the cage is 4 GB-aligned, the cage base's low 32 bits are zero. Decoding a pointer is therefore just cage_base + compressed:

Address result = cage_base + static_cast<Address>(raw_value);
V8_ASSUME(static_cast<uint32_t>(result) == raw_value);
return result;

src/common/ptr-compr-inl.h#L115-L130

The cage base is kept in a fixed register on the hot paths, so decompression compiles to one add that out-of-order execution largely hides. Compression (storing a pointer) is even cheaper: truncate to 32 bits.

::: details How is the cage base known? For most objects V8 uses a single per-isolate (or shared) cage, and the base is loaded from the isolate root / a dedicated register. The scheme is implemented as V8HeapCompressionScheme over a MainCage holder; see src/common/ptr-compr.h#L59-L74. :::

It composes with Smis

Recall from tagged values that on a 64-bit build the tagged word is now 32 bits, and a Smi occupies the upper bits with the low bit as tag. So the same 32-bit slot holds either a 31/32-bit Smi or a 32-bit compressed pointer, distinguished by the tag bit. Compression and tagging reinforce each other: both shrink the per-value footprint to 4 bytes.

Why it pays off

  1. ~2× smaller pointer-heavy objects. A {a, b, c, d} object, a Map, a FixedArray — all are mostly tagged slots. Halving each slot is a large, broad memory saving across millions of objects.

  2. Better cache behavior. More objects per cache line and per page means fewer misses. On memory-bound workloads this often speeds the program up, even though each load now does an extra add — the cache wins dominate.

  3. Less GC work. Fewer bytes of pointers to scan, mark, and update during collection; remembered-set slots are smaller too. See GC overview.

The trade-off is the obvious one: the managed heap is capped at 4 GB per cage, and every decompression costs an add. For V8's workloads that is a clear win, which is why pointer compression is on by default on 64-bit.

Relationship to the sandbox

The pointer-compression cage sits at the front of the larger V8 sandbox region. Constraining heap references to 32-bit intra-cage offsets is also a security property: a corrupted pointer can only ever address within the cage, not arbitrary process memory. Memory efficiency and sandboxing share the same mechanism.

See also