Accelerating JavaScript Performance: How V8 Optimized Mutable Heap Numbers for a 2.5x Boost

Introduction

At V8, our mission to continuously enhance JavaScript performance led us to revisit the JetStream2 benchmark suite, aiming to eliminate performance cliffs. One optimization in particular stood out: a modification to the handling of mutable heap numbers within the async-fs benchmark resulted in a remarkable 2.5x speedup and a noticeable improvement in the overall benchmark score. While inspired by this benchmark, the pattern appears in real-world code as well, making this optimization broadly beneficial.

Accelerating JavaScript Performance: How V8 Optimized Mutable Heap Numbers for a 2.5x Boost
Source: v8.dev

The async-fs Benchmark and Its Peculiar Math.random

The async-fs benchmark is a JavaScript filesystem implementation that focuses on asynchronous operations. Surprisingly, its performance bottleneck lies in the custom implementation of Math.random used to ensure deterministic results across runs. The implementation repeatedly updates a variable named seed using a series of arithmetic and bitwise operations.

The critical element here is that seed is stored in a ScriptContext—an internal storage location for values accessible within a particular script. Internally, V8 represents this context as an array of tagged values. On 64-bit systems, each tagged value occupies 32 bits, with the least significant bit acting as a tag. A 0 indicates a 31-bit Small Integer (SMI), which stores the integer value directly, left-shifted by one bit. A 1 indicates a compressed pointer to a heap object, where the pointer is incremented by one.

This tagging scheme distinguishes how numbers are stored: SMIs reside directly in the ScriptContext, while larger numbers or those with fractional parts are stored indirectly as immutable HeapNumber objects on the heap. The ScriptContext then holds a compressed pointer to that HeapNumber. This approach efficiently handles various numeric types while optimizing for the common SMI case.

The Bottleneck: Immutable HeapNumber Allocations

Profiling Math.random revealed two major performance issues:

  1. HeapNumber allocation: The slot dedicated to the seed variable in the script context points to a standard, immutable HeapNumber. Each time the custom Math.random function updates seed, a new HeapNumber object must be allocated on the heap because the existing one cannot be mutated. This allocation occurs on every call, generating significant overhead.
  2. Memory pressure: Repeated allocations and subsequent garbage collection of old HeapNumbers increase memory pressure and slow down execution.

These issues collectively created a performance cliff that was particularly pronounced in the async-fs benchmark’s tight loop of Math.random calls.

The Optimization: Mutable Heap Numbers

To eliminate these allocations, the V8 team introduced a new type of numeric storage: the mutable heap number. Instead of always placing numbers on the heap as immutable objects, the system now allows certain slots—like the one used for seed in the ScriptContext—to hold a mutable heap number. This mutable object can be updated in place without allocating a new HeapNumber each time.

By converting the seed slot to use a mutable heap number, the optimization bypasses the allocation overhead entirely. The same heap object is reused, and its numeric value is updated directly. This change removed the primary bottleneck in the Math.random path.

Impact on the Benchmark

The result was a 2.5x improvement in the async-fs benchmark’s score. This single optimization contributed a noticeable boost to the overall JetStream2 score, demonstrating the outsized effect that a small change can have when applied to a frequently executed code path.

Relevance to Real-World Code

While the optimization was inspired by a benchmark, mutable heap numbers benefit real-world JavaScript applications that exhibit similar patterns—such as repeatedly updating a numeric variable that exceeds SMI range or involves fractional values. Common scenarios include:

  • Game loops that update position, velocity, or other floating-point state variables.
  • Scientific computations with iterative updates to large numbers.
  • Simulation and machine learning workloads that frequently modify numeric arrays or counters.

By reducing allocation pressure and garbage collection pauses, V8’s mutable heap number optimization delivers smoother performance in these use cases.

Conclusion

The introduction of mutable heap numbers in V8 is a prime example of how deep profiling and targeted optimization can eliminate performance cliffs. The 2.5x speedup observed in the async-fs benchmark underscores the importance of adapting internal data representations to match actual usage patterns. As V8 continues to evolve, optimizations like these ensure that JavaScript remains fast and efficient for both benchmarks and real-world applications.

For further details, refer to the async-fs math.random section above or explore other V8 performance articles.

Tags:

Recommended

Discover More

Design Principles: A Framework for Coherent Product Decisions10 Critical Facts About the Canvas Cyberattack That Disrupted Final ExamsNavigating Legal Hurdles in Medicare Advantage Fraud Investigations: A Step-by-Step GuideHow to Get Ready for the Next Generation of iPads: A Rumour-Based Preparation GuideDefending vSphere Against BRICKSTORM Malware: Key Questions and Answers