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
- UK Slashes Green Climate Fund Pledge, Ceding Top Donor Status Amid Aid Cuts
- Ford Pivots to Grid Storage: New Subsidiary Targets 20 GWh Annual Production
- Flutter's 2026 Global Tour: Opportunities to Connect with the Core Team
- 10 Key Developments Behind Tesla’s Decision to Abandon Its India Factory Plans
- Kia's Electric Vehicle Surge: Record US Sales and the Anticipated EV3 Launch
- Electric Vehicle Sales Soar: IEA Predicts 23 Million EVs by 2026
- Hamburg Leads the Charge: 240 Electric Buses to Join Fleet by 2031
- Sardinia's Renewable Energy Revolt: Why Islanders Say No to Wind and Solar