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
~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.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.
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
Tagged values & Smis — the encoding compression builds on.
The V8 sandbox — the cage as a security boundary.
GC overview — why smaller pointers mean cheaper collection.