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.
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:
- HeapNumber allocation: The slot dedicated to the
seedvariable in the script context points to a standard, immutable HeapNumber. Each time the customMath.randomfunction updatesseed, 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. - 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.
Related Articles
- Flutter and Dart Get Agent AI Skills to Close Knowledge Gap
- Implementing Honda Mobile Power Pack e: Battery Swap for Your Fleet – A Business Guide
- Using the Hydrogenosome Discovery to Slash Livestock Methane Emissions
- Green Deals Roundup: ENGWE Anniversary, Lectric Mother's Day, Segway Scooter Low, and More EV Savings
- Mastering Dart and Flutter Development with AI-Powered Skills
- Rivian Scales Back Georgia EV Factory to 300K Units After DOE Loan Cut to $4.5B
- Navigating Away from Sea of Nodes: V8's Move to Turboshaft
- Tesla’s Full Self-Driving (Supervised) Gains Limited Approval in Belgium