How V8's Mutable Heap Numbers Boost JavaScript Performance by 2.5x
Introduction
In the relentless pursuit of faster JavaScript execution, the V8 team recently turned its attention to the JetStream2 benchmark suite, aiming to eliminate performance cliffs that could hinder real-world applications. This deep dive led to a breakthrough optimization — introducing mutable heap numbers — which delivered a striking 2.5× improvement in the async-fs benchmark and a noticeable lift in the overall score. While inspired by a benchmark, the pattern it addresses appears in production code, making this fix both practical and impactful.
The Problem: Heap Allocation in Math.random
The async-fs benchmark, a JavaScript file system simulation focused on asynchronous operations, harbored an unexpected bottleneck: its implementation of Math.random. For reproducibility, the benchmark uses a custom, deterministic pseudo-random number generator. The heart of this generator is a seed variable, updated on every call:
let seed; Math.random = (function() { return function () { seed = ((seed + 0x7ed55d16) + (seed << 12)) & 0xffffffff; seed = ((seed ^ 0xc761c23c) ^ (seed >>> 19)) & 0xffffffff; seed = ((seed + 0x165667b1) + (seed << 5)) & 0xffffffff; seed = ((seed + 0xd3a2646c) ^ (seed << 9)) & 0xffffffff; seed = ((seed + 0xfd7046c5) + (seed << 3)) & 0xffffffff; seed = ((seed ^ 0xb55a4f09) ^ (seed >>> 16)) & 0xffffffff; return (seed & 0xfffffff) / 0x10000000; }; })();How ScriptContext Stores Values
The
seedvariable lives in the ScriptContext, which V8 uses to store per-script values. Internally, this context is an array of tagged values. On 64-bit systems, each tag occupies 32 bits. The least significant bit indicates whether the value is a Small Integer (SMI, tag bit 0) or a compressed pointer to a heap object (tag bit 1). SMIs store 31-bit integers directly in the slot; non-SMI numbers — those with fractional parts or large magnitudes — are stored as HeapNumber objects on the heap, with the slot holding a pointer to an immutable double value.The Bottleneck
Profiling
Math.randomrevealed two major performance issues:
- HeapNumber allocation: Because the
seedvalue is a double (the result of arithmetic involving fractional division), it cannot be stored as an SMI. Instead, every update toseedcreates a new immutable HeapNumber on the heap — a costly allocation that happens on every single call. - Garbage collection pressure: These short-lived HeapNumbers accumulate rapidly, increasing GC overhead and further degrading performance.
The Solution: Mutable Heap Numbers
The V8 team recognized that the seed variable is updated frequently and its lifetime is tied to the script — there is no need for immutability. The fix was to introduce mutable heap numbers for certain ScriptContext slots. Instead of allocating a new HeapNumber each time, V8 can now modify the double in-place, reusing the same heap memory.
This optimization works by detecting slots that are frequently updated with non-SMI numeric values. When such a pattern is found, V8 allocates a mutable HeapNumber and stores the pointer in the ScriptContext. Subsequent assignments directly overwrite the double value, eliminating allocation and reducing GC pressure.
Implementation Details
- Detection: The optimizing compiler identifies ScriptContext loads/stores that involve double arithmetic and frequent updates.
- Allocation: A special mutable HeapNumber is created once, with a flag indicating mutability.
- In-place update: On assignment, the double value is written directly into the existing HeapNumber object.
- This change required careful handling of concurrent access and garbage collection invariants, but the payoff was substantial.
Results and Impact
After implementing mutable heap numbers, the async-fs benchmark saw a 2.5× speedup. The overall JetStream2 score also improved noticeably. More importantly, the technique benefits any JavaScript code that frequently updates a non-integer numeric variable stored in a closure or script context — a pattern common in game loops, simulations, and scientific computations.
The optimization reduced heap allocation in the benchmark by over 90% for the Math.random calls, and garbage collection pauses dropped significantly. Real-world applications using similar patterns can expect smoother performance.
Conclusion
V8's mutable heap numbers is a clever optimization that transforms a performance cliff into a smooth ride. By recognizing that immutable HeapNumbers are overkill for frequently-updated context variables, V8 reclaims the speed that unnecessary allocation once stole. This improvement exemplifies how targeted analysis of benchmark behavior can lead to wins that resonate far beyond the test suite, making everyday JavaScript applications faster.
Related Articles
- How to Oppose an EU Trademark Application: Lessons from Apple's Citrus Logo Dispute
- How to Build a Full-Stack Dart App with Firebase Functions and GenUI
- BYD April Exports Surpass Tesla Global Sales in Historic EV Milestone
- Amazon's Electric Cargo Bikes: A Sustainable Shift for Urban Deliveries in Washington, D.C.
- The Quiet Revolution: How Japan's Motorcycle Titans Are Shifting to Electric
- Tesla Semi Reaches Production Milestone: First Truck Rolls Off Assembly Line
- How to Decode Ancient Copper Smelting Signs in Cave Sites
- 6 Revolutionary Facts About Making Cement from a Different Rock to Cut CO2