● LIVE   Breaking News & Analysis
Ehedrick
2026-05-20
Environment & Energy

8 Crucial Facts About V8's Mutable Heap Number Benchmark Boost

V8's optimization of mutable heap numbers yielded a 2.5x speedup in the JetStream2 async-fs benchmark by eliminating costly HeapNumber allocations in a custom Math.random implementation.

At V8, we are always looking for ways to make JavaScript faster. Recently, we revisited the JetStream2 benchmark suite to eliminate performance cliffs. This article details a specific optimization that led to a remarkable 2.5x improvement in the async-fs benchmark, significantly boosting the overall score. Inspired by the benchmark, the optimization addresses patterns that appear in real-world code. Here are eight key facts you need to know about this optimization.

1. The Performance Drive Behind V8's Optimizations

V8’s engineering team constantly works to enhance JavaScript performance. The JetStream2 suite is a critical tool for measuring real-world speed. In our analysis, we spotted a performance cliff in the async-fs benchmark. This benchmark simulates a file system in JavaScript, focusing on asynchronous operations. The bottleneck was surprising: it stemmed from the custom Math.random implementation used in the benchmark. Addressing it yielded a huge speedup and taught us valuable lessons about heap number handling.

8 Crucial Facts About V8's Mutable Heap Number Benchmark Boost
Source: v8.dev

2. Uncovering a Surprising Bottleneck in Math.random

The async-fs benchmark uses a deterministic Math.random for consistent results. The implementation relies on a global variable seed that is updated on every call. This custom algorithm runs several bitwise operations to generate the next pseudo-random number. The critical detail: seed is stored in a ScriptContext. While seemingly innocuous, this storage method creates a hidden performance issue that becomes severe under heavy use.

3. How ScriptContext Stores Global Variables

In V8, a ScriptContext holds values accessible within a specific script. Internally, it is an array of tagged values. On 64-bit systems with default V8 configuration, each tagged value occupies 32 bits. The least significant bit acts as a tag: 0 indicates a Small Integer (SMI), while 1 indicates a compressed pointer to a heap object. The seed variable is stored as a pointer to a HeapNumber, which holds the double-precision floating-point value.

4. The Intricacies of Tagged Value Representations

Tagged values allow V8 to efficiently distinguish between different types. SMIs are stored directly in the context array, left-shifted by one bit. But when a number is too large or has a fractional part, it becomes a HeapNumber – an immutable 64-bit double allocated on the heap. The ScriptContext stores only a compressed pointer to this HeapNumber. This design is great for common small integers, but it creates an extra indirection for larger numbers.

5. The Hidden Cost of Immutable HeapNumbers

HeapNumbers are immutable. When the Math.random function updates seed, it cannot modify the existing HeapNumber. Instead, it must allocate a brand new HeapNumber on the heap and store the pointer in the ScriptContext. This allocation happens every single time Math.random is called. In a benchmark that calls it repeatedly, this leads to massive memory allocation and garbage collection pressure.

6. Profiling Reveals Two Major Performance Issues

Profiling the Math.random implementation uncovered two related problems. First, the constant allocation of new HeapNumber objects caused significant memory pressure and GC overhead. Second, each update required writing a new pointer into the ScriptContext, adding extra store instructions. Combined, these issues transformed a seemingly simple number update into an expensive operation, severely impacting the benchmark’s performance.

7. The Optimization: Mutable Heap Numbers

To eliminate the bottleneck, V8 introduced support for mutable HeapNumber objects. Instead of allocating a new object on every update, V8 allows the existing HeapNumber to be mutated in place if it is only referenced by the ScriptContext. This change eliminates the allocation and reduces GC pressure. The optimization is safe because the engine ensures that no other references to the HeapNumber exist at the point of mutation.

8. The Impact: 2.5x Speedup and Broader Lessons

The result was a 2.5x speedup in the async-fs benchmark and a noticeable boost to the overall JetStream2 score. More importantly, the optimization benefits real-world code that exhibits similar patterns – for example, Monte Carlo simulations or game engines that frequently update global number variables. This experience reaffirms that even small, seemingly internal changes can have outsized impacts on performance.

In conclusion, V8’s mutable heap number optimization shows how deeply understanding runtime behavior can lead to significant gains. By addressing a hidden allocation bottleneck, we transformed a benchmark performance cliff into a smooth, fast experience. These learnings will continue to guide future work, ensuring JavaScript remains a performant choice for demanding applications.