How SMI Array Packing Optimizes JavaScript Memory

Modern JavaScript engines utilize Small Integer (SMI) packing to store whole numbers directly in memory without heap allocation overhead. In engines like Google’s V8, numbers are treated dynamically, but keeping arrays populated strictly with small integers allows the runtime to switch to optimized, unboxed storage representations. This article explains how the SMI representation works, how JavaScript engines organize packed arrays, and why this design significantly reduces memory consumption and improves CPU cache efficiency.

The Concept of Small Integers (SMIs)

In standard JavaScript, the language specification treats all numbers as double-precision 64-bit binary floating-point values (IEEE 754). Allocating 64 bits on the heap for every single number would lead to severe memory bloat and garbage collection pressure.

To prevent this, engines use a technique called pointer tagging. In 64-bit architectures, memory addresses are typically aligned to word boundaries, leaving the least significant bits unused. The engine uses the lowest bit as a tag: * Pointers to heap objects have their lowest bit set to 1. * SMIs (Small Integers) have their lowest bit set to 0.

Because of this tag, an integer (typically signed 31-bit or 32-bit depending on the architecture) is stored directly inside the pointer field itself. It requires no heap allocation, no object header, and zero garbage collection overhead.

Array Element Kinds: PACKED_SMI_ELEMENTS

Engines track the contents of arrays through dynamic categories known as Elements Kinds. When an array is initialized and populated solely with small integers, the engine tags it internally as PACKED_SMI_ELEMENTS.

const numbers = [1, 2, 3, 4, 5]; // PACKED_SMI_ELEMENTS

In this state: 1. Contiguous Storage: The engine allocates a contiguous buffer in memory containing raw SMIs. 2. No Object Wrappers: Unlike generic arrays that hold references pointing to distinct HeapNumber objects on the heap, the SMI array stores the values inline. 3. No Sentinel Values: Because the array is “packed” (dense), every index from 0 to length - 1 contains a valid value, avoiding overhead associated with missing keys or holes.

How Packed SMIs Save Memory

1. Elimination of Heap Allocations

If an array stored standard floating-point numbers outside the SMI range or arbitrary objects, each unique number could require a HeapNumber allocation. A HeapNumber includes a map pointer, object flags, and the 64-bit float itself (consuming roughly 16 to 24 bytes per number). Storing integers as SMIs eliminates this heap wrapper entirely, reducing the footprint to just the size of the pointer slot (4 to 8 bytes).

2. Elimination of Indirection

In a generic array (PACKED_ELEMENTS), the array buffer contains pointers, each directing the runtime to a different location in heap memory. With PACKED_SMI_ELEMENTS, the indirection layer is gone. The value retrieved from the array slot is the number, cutting the total memory footprint in half or better.

3. High CPU Cache Locality

Because SMIs are stored in a contiguous, flat block of memory, reading elements sequentially allows the CPU to fetch multiple values inside a single L1/L2 cache line. This reduces cache misses, which indirectly minimizes the memory bandwidth required during data processing loops.

The Cost of Element Transitions

Array optimizations are one-directional. If an array tagged as PACKED_SMI_ELEMENTS receives a non-SMI value, the engine must convert the underlying memory structure to a more generic format:

Once an array transitions to a more generic type, it cannot transition back to PACKED_SMI_ELEMENTS, permanently increasing the memory allocation and GC overhead for that array instance.

Best Practices for Memory-Efficient Arrays