Smi (Small Integer) optimization in V8 is a technique where small integers are encoded directly into the tagged pointer representation, allowing them to be stored and manipulated as immediate values without heap allocation, dramatically improving performance.
Smi (Small Integer) optimization is a fundamental technique in V8 where integers within a certain range are stored directly in the tagged pointer value itself, rather than as separate heap-allocated objects. This is achieved through pointer tagging, where the least significant bit (LSB) of a value distinguishes between SMIs (bit 0) and pointers to heap objects (bit 1) . This clever encoding allows V8 to perform integer operations with minimal overhead, as SMIs can be manipulated directly in registers without expensive memory allocations or garbage collection.
Tagging Scheme: V8 uses the least significant bit as a tag: 0 indicates a Small Integer (SMI), and 1 indicates a pointer to a heap object . This allows instantaneous type determination with a single bit test.
Encoding on 64-bit systems: SMIs are stored as 32-bit integer values shifted left by 32 bits, leaving the LSB as 0 . The actual integer value is recovered by right-shifting the tagged value by 32 bits.
Encoding on 32-bit systems: SMIs use 31 bits for the value plus a 0 tag bit, supporting signed 31-bit integers .
Pointer encoding: Heap object pointers have their LSB set to 1 and must be masked (pointer = tagged_value & ~1) to obtain the actual memory address .
Constant-time checks: V8 can determine if a value is an SMI simply by checking (value & 1) == 0, enabling extremely fast type feedback in the interpreter and optimizing compiler .
The SMI range is platform-dependent but generally covers signed 31-bit or 32-bit integers. On 64-bit systems, SMIs are typically 32-bit signed integers ranging from -2,147,483,648 to 2,147,483,647 . Values outside this range, or numbers with fractional parts, must be represented as HeapNumbers—actual heap-allocated objects that carry the overhead of memory allocation, garbage collection, and indirection .
No heap allocation: SMIs are stored directly in the value, eliminating memory allocation and garbage collection overhead .
Register-friendly: SMIs can be manipulated directly in CPU registers without memory indirection, making arithmetic operations extremely fast .
Cache efficiency: Because SMIs are embedded directly in objects (e.g., array elements or object properties), they improve spatial locality and cache utilization .
Array specialization: V8 can optimize arrays that contain only SMIs using the PACKED_SMI_ELEMENTS kind, enabling highly optimized operations .
Deoptimization avoidance: When values remain SMIs, the engine can maintain monomorphic type feedback and avoid costly deoptimizations .
A critical aspect of SMI optimization is that arrays and objects track their elements kind based on the types they contain. When an array contains only SMIs, V8 marks it as PACKED_SMI_ELEMENTS, which enables the most aggressive optimizations. However, adding any non-SMI value—a floating-point number, a value outside the SMI range, or even -0 (negative zero)—forces the array to transition to a more general elements kind (PACKED_DOUBLE_ELEMENTS or PACKED_ELEMENTS), and crucially, these transitions are one-way: an array can never go back to being a SMI array once it has been generalized .
Array indexing: Using SMIs for array indices is significantly faster than using doubles or other numeric types .
Loop performance: When loop counters stay within SMI range, the engine can optimize iterations aggressively. Exceeding the SMI range mid-loop can cause deoptimization .
Object properties: Objects with SMI-valued properties can be optimized more effectively, as the property values can be stored inline rather than as separate HeapNumber objects .
Memory savings: A SMI array element occupies exactly the tagged pointer size (usually 4 or 8 bytes), while a HeapNumber element requires the tagged pointer plus a separate heap allocation for the number object .
From an implementation perspective, SMIs are not "pooled" or interned like strings—each SMI value is encoded directly where it appears . When an object has a property like {count: 42}, the value 42 is encoded as an SMI and stored directly in the object's property storage. If the value were 42.5, the object would store a pointer to a separate HeapNumber object elsewhere on the heap . This is why SMI optimization matters: it keeps data compact, reduces pointer chasing, and enables the CPU to work directly with values rather than chasing references through memory.
For developers writing performance-critical JavaScript, the key takeaway is to maintain SMI-friendly code when possible. This means staying within the 32-bit signed integer range (avoid numbers that exceed ±2³¹-1), avoiding decimal fractions in hot paths, and being cautious with operations like -0, NaN, or Infinity that immediately force transitions to more general (and slower) representations . Tools like V8's --trace-elements-transitions flag can help observe when arrays change their elements kind, providing insight into where optimizations may be lost .