Double Elements vs Tagged Pointers in JS Arrays
JavaScript engines optimize array storage in memory by categorizing arrays into different internal “element kinds” based on the data they contain. This article breaks down the fundamental differences between double elements and tagged pointers in JavaScript engines (such as Google V8), detailing how each format represents data, handles memory layout, and influences runtime performance.
Tagged Pointers in JavaScript Arrays
In modern JavaScript engines, values like Small Integers (Smis) and references to objects are represented using pointer tagging. Because memory addresses are aligned to word boundaries (typically 4 or 8 bytes), the least significant bits of a memory address are always zero. JavaScript engines exploit these unused bits as a “tag” to distinguish between immediate integer values and heap pointers without requiring extra memory.
When an array contains integers within a specific range (e.g., 31-bit signed integers) or references to JavaScript objects, it uses tagged pointer elements:
- Small Integers (Smis): The engine encodes the
number directly within the pointer word, tagged with a specific bit
(usually a
0in the least significant bit). This avoids heap allocation entirely. - Heap Objects: If the element is a string, object,
or larger number, the value is stored as a memory address pointing to
the heap, tagged with a different bit (typically a
1).
In V8, arrays containing only Smis use the
PACKED_SMI_ELEMENTS kind, while arrays containing general
objects use PACKED_ELEMENTS.
Double Elements in JavaScript Arrays
Floating-point numbers in JavaScript conform to the 64-bit IEEE 754
standard. Normally, numbers that cannot fit into the Smi representation
must be allocated on the heap as HeapNumber objects, which
introduces memory overhead and pointer indirection.
To optimize arrays of floating-point numbers, engines implement
double elements (such as
PACKED_DOUBLE_ELEMENTS in V8):
- Unboxed Storage: Instead of creating an array of
tagged pointers pointing to
HeapNumberobjects on the heap, the engine stores the raw, unboxed 64-bit floating-point values directly in a contiguous block of memory. - Flattened Memory: Each element is an actual 8-byte float rather than a reference, drastically reducing memory footprint and eliminating garbage collection pressure caused by intermediate number objects.
Core Differences
| Feature | Double Elements | Tagged Pointers |
|---|---|---|
| Data Format | Raw, unboxed 64-bit IEEE 754 floats | Bit-tagged integer values or heap memory addresses |
| Heap Allocation | None for individual floating-point values | Required for values that cannot be represented as Smis |
| Element Kind (V8) | PACKED_DOUBLE_ELEMENTS /
HOLEY_DOUBLE_ELEMENTS |
PACKED_SMI_ELEMENTS or
PACKED_ELEMENTS |
| Primary Use Case | Arrays composed exclusively of non-integer numbers | Arrays of integers, mixed types, objects, or strings |
Transitions and Performance Implications
Array element kinds transition unidirectionally from the most specific to the most general:
- An array initialized with small integers starts as a tagged integer
array (
PACKED_SMI_ELEMENTS). - Pushing a floating-point number (e.g.,
4.5) causes the engine to reallocate and convert the array to a double array (PACKED_DOUBLE_ELEMENTS), unboxing the existing numbers into raw 64-bit floats. - Pushing an object or string transitions the array to a general
tagged pointer array (
PACKED_ELEMENTS). In this state, any floating-point numbers must be boxed back intoHeapNumberobjects on the heap.
Double elements provide substantial performance gains for numerical computation by avoiding boxing, whereas tagged pointers provide the flexibility necessary to hold diverse object types and fast immediate integers in a single array structure.