How JavaScript Implements BigInt for Large Numbers
JavaScript’s BigInt primitive allows developers to
represent and manipulate integers beyond the safe limit of the standard
Number type (\(2^{53} -
1\)). While standard numbers use 64-bit double-precision
floating-point format (IEEE 754), JavaScript engines like V8 and
SpiderMonkey implement BigInt using arbitrary-precision
arithmetic (often called “bignum”). This architecture splits large
values across dynamically allocated arrays of machine-word integers and
uses specialized algorithms to compute results across those
segments.
The Memory Representation: Digits and Limbs
Standard CPU registers operate on fixed bit-widths (typically 32-bit
or 64-bit). Because a BigInt can have virtually unlimited
magnitude, the engine cannot store it in a single hardware register.
Instead, JavaScript engines allocate an internal data structure containing: * A sign bit: Indicates whether the number is positive or negative. * A length indicator: Tracks the number of chunks currently in use. * An array of “digits” (or “limbs”): The raw numeric value stored in base \(2^{32}\) (on 32-bit platforms) or base \(2^{64}\) (on 64-bit platforms).
For example, on a 64-bit architecture, a number requiring 128 bits is stored as two consecutive 64-bit integers. If an operation exceeds the capacity of the current limbs, the engine dynamically reallocates memory to add another limb.
Heap Allocation and Immutability
Unlike primitive Number values, which engines can often
store inline directly on the stack or inside CPU registers,
BigInt instances are typically heap-allocated objects under
the hood.
Despite being stored on the heap like objects, BigInt
retains primitive semantics in JavaScript: *
Immutability: Every arithmetic operation creates a
completely new BigInt structure rather than mutating
existing limbs. * Garbage Collection: Intermediate
BigInt values generated during complex computations must be
collected by the JavaScript garbage collector once they fall out of
scope.
Some engines apply small-integer optimizations. When a
BigInt value fits inside standard pointer bits (using
pointer tagging techniques), engines can avoid a full heap allocation,
though many implementations keep all BigInt representations
unified on the heap for consistency.
Arithmetic Implementation
Because the hardware CPU does not have instructions for arbitrary-precision math, JavaScript engines implement arithmetic using multi-precision algorithms:
- Addition and Subtraction: The engine iterates through the limbs from least significant to most significant, applying standard bitwise addition while tracking and carrying the overflow bit to the next limb.
- Multiplication: For smaller
BigIntvalues, engines generally use the standard schoolbook multiplication algorithm (\(O(n^2)\)). As numbers grow larger, engines switch to asymptotically faster algorithms, such as Karatsuba multiplication (\(O(n^{1.58})\)), to reduce compute time. - Division and Modulo: Implemented using multi-precision division algorithms, such as Knuth’s Algorithm D, which iteratively estimates quotient digits and normalizes remainders across limbs.
Performance Trade-Offs
Because BigInt relies on software-level emulation rather
than direct hardware instructions, its operations are significantly
slower than standard 64-bit floats or 32-bit integers. The overhead
comes from dynamic memory management, cache misses associated with
pointer-chasing on the heap, and multi-step loop execution for
multi-limb arithmetic.