What Are SMI Small Integers in JavaScript Engines?
Small Integers, commonly known as SMIs (pronounced “smees”), are a critical internal optimization used by JavaScript engines like Google’s V8, SpiderMonkey, and JavaScriptCore to represent integer values efficiently. Although the ECMAScript specification defines all numbers as standard 64-bit floating-point values (doubles), allocating a full 64-bit float or creating a heap object for simple operations—like loop counters or array indices—causes severe memory overhead and performance degradation. JavaScript engines solve this by using pointer tagging to store integers directly within pointer registers without heap allocation, significantly speeding up execution and reducing memory usage.
The Problem with JavaScript Numbers
According to the ECMAScript specification, the number
type is an IEEE 754 standard 64-bit double-precision floating-point
value. In traditional implementations, storing complex data requires
allocating memory on the heap and passing around memory addresses
(pointers).
If a JavaScript engine created a heap-allocated object
(HeapNumber) for every single integer calculation, memory
allocations and Garbage Collection (GC) pauses would quickly cripple
performance.
What Is an SMI?
An SMI is a signed integer that fits within a specific range, typically 31 bits on 32-bit systems and 31 or 32 bits on 64-bit architectures.
Instead of treating integers as full 64-bit floating-point numbers on the heap, engines treat SMIs as immediate, unboxed values. They are stored directly inside the variable slot or CPU register rather than being wrapped in an object or referenced through a memory pointer.
How Pointer Tagging Enables SMIs
Modern computers align memory addresses for heap objects to 4-byte or
8-byte boundaries. Because these addresses are multiples of 4 or 8,
their binary representations always end in at least two or three zero
bits (e.g., ...00 or ...000).
JavaScript engines take advantage of these unused low-order bits through a technique called pointer tagging:
- Pointer to a Heap Object: The engine sets the least
significant bit (LSB) to
1. When reading the value, the CPU clears this bit to recover the actual memory address of the object. - SMI (Small Integer): The engine sets the LSB to
0. This informs the engine that the payload is not a memory address, but the actual integer value itself.
Representation in Memory
- On a 32-bit architecture: A 31-bit signed integer
is shifted to the left by 1 bit, leaving the lowest bit as
0:[ 31-bit Signed Integer Payload ] [ 0 ] - On a 64-bit architecture: The engine can either shift a 32-bit integer and pad it, or use pointer compression where the top 32 bits store the signed integer and the lower 32 bits contain the tag bit.
Performance Benefits of SMIs
- Zero Heap Allocation: Because SMIs are stored directly in the reference slot, no dynamic memory is allocated on the heap, completely bypassing the garbage collector.
- Fast Arithmetic: Basic math operations can be executed with minimal overhead. For instance, addition can often be performed directly on tagged values, or by simply shifting the bits to remove the tag, executing the CPU instruction, and shifting back.
- Memory Density: Arrays containing only SMIs (known
in V8 as
PACKED_SMI_ELEMENTS) are stored as contiguous arrays of unboxed integers, resulting in maximum cache locality and minimal memory footprint.
Transitioning Out of SMI
When an integer exceeds the maximum SMI range (e.g., surpassing \(2^{30} - 1\) on a 31-bit system), or when a
fractional/floating-point operation occurs, the engine automatically
converts the SMI into a boxed HeapNumber (a
double-precision float on the heap) or a 64-bit float representation.
This process is transparent to the developer, though keeping numeric
values within the SMI range allows JavaScript code to run at peak
optimization.