Boosting V8 Performance: Optimizing Mutable Heap Numbers in JavaScript Engines
Introduction
At the V8 engine team, performance is always a top priority. In our continuous quest to make JavaScript faster, we recently analyzed the JetStream2 benchmark suite to identify and eliminate performance cliffs. One particular optimization stood out: we reworked how certain numeric values are stored and updated in the engine's ScriptContext, leading to a remarkable 2.5× speedup in the async-fs benchmark and a measurable boost to the overall JetStream2 score. While the optimization was inspired by a specific benchmark, the underlying pattern—frequent updates to a numeric variable—appears in real-world applications as well.
The Benchmark and the Culprit
The async-fs benchmark simulates a JavaScript-based file system with a focus on asynchronous operations. However, its real performance bottleneck turned out to be the implementation of Math.random. To ensure deterministic and reproducible results, the benchmark uses a custom pseudo-random number generator (PRNG) defined as follows:
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;
};
})();
The variable seed is updated on every call to Math.random, generating the pseudo-random sequence. In V8's internal architecture, this seed is stored in a ScriptContext—an array of tagged values that holds variables accessible from a script. On 64-bit systems, each slot in the ScriptContext occupies 32 bits. The least significant bit acts as a tag: 0 indicates a 31-bit Small Integer (SMI) (stored directly, left-shifted), while 1 indicates a compressed pointer to a heap object.
This tagging scheme means numbers are stored in two fundamentally different ways:
- SMIs reside directly in the ScriptContext slot.
- HeapNumbers (floating-point or larger integers) are stored as 64-bit double-precision values on the heap, with the ScriptContext holding a compressed pointer to an immutable HeapNumber object.
The Bottleneck: HeapNumber Allocation
Profiling the Math.random function revealed two major performance issues, both caused by the way seed is stored:
- HeapNumber allocation: Since
seedholds a value that is not a SMI (because the bitwise operations produce numbers that may exceed the SMI range or have a decimal component in the final step), the ScriptContext slot originally pointed to a standard immutable HeapNumber. Each call toMath.randomcomputes a new value forseed, forcing the engine to allocate an entirely new HeapNumber object on the heap. This allocation occurs on every invocation, resulting in significant memory and performance overhead. - Garbage collection pressure: With each allocation, the old HeapNumber becomes garbage, increasing the frequency and intensity of garbage collection cycles.
These two factors combined created a severe performance cliff. The benchmark spent a disproportionate amount of time in allocation and GC routines, rather than in the actual PRNG computation.
The Solution: Introducing Mutable Heap Numbers
To tackle this, we redesigned the representation of certain numeric slots in the ScriptContext. Instead of forcing an immutable HeapNumber when a variable cannot be stored as a SMI, we introduced mutable heap numbers—special HeapNumber objects whose double-precision value can be updated in place.
The key insight is that when a numeric variable is updated repeatedly in a tight loop (as seed is), allocating a new immutable object each time is wasteful. By allowing the ScriptContext slot to directly hold a mutable HeapNumber, the engine can:
- Update the 64-bit double value inside the existing object without allocating new memory.
- Avoid generating garbage, thereby reducing GC pressure.
- Keep the same pointer in the ScriptContext slot, only modifying its contents.
Technically, this was implemented by adding a flag to the HeapNumber object indicating mutability. When the V8 compiler detects a pattern where a numeric property in a context is frequently reassigned, it can allocate a mutable HeapNumber instead of the default immutable one. The necessary write barriers are still honored to maintain correctness with V8's generational garbage collector.
Results: A 2.5× Speedup
After implementing mutable heap numbers for the seed variable in the async-fs benchmark, we observed a 2.5× performance improvement in that benchmark alone. This contributed a noticeable boost to the overall JetStream2 score. The optimization is now part of V8's runtime and benefits any similar code pattern—namely, variables that hold numeric values larger than 31 bits or with fractional parts and are updated frequently.
Conclusion
This work underscores the importance of examining seemingly simple operations under the hood. A seemingly trivial variable assignment—seed = ...—can become a bottleneck when implemented naively. By moving from immutable to mutable heap numbers, V8 eliminated unnecessary allocations and GC overhead, delivering a substantial speedup in both synthetic benchmarks and real-world applications that exhibit the same pattern.
Related Articles
- The Ultimate College Laptop Guide: Find Your Perfect Academic Partner
- How to Integrate Honda’s Mobile Power Pack e: Battery Swap System into Your Commercial Fleet
- Flutter Takes Center Stage at Google Cloud Next 2026: Full-Stack Dart Preview and AI-Powered Experiences Unveiled
- React Native 0.85 Released: Shared Animation Backend Launches, Jest Preset Separated, and More
- How Defense Contracts Are Fueling Next-Gen EV Battery Development
- Kingman, Arizona: Where Route 66 History Meets Electric Vehicle Travel
- Orion PDA: A Retro-Inspired Handheld Computer with Solar Charging and Sunlight-Readable Screen
- Rivian Secures $4.5 Billion DOE Loan for Georgia EV Factory Despite Policy Uncertainty