How V8's Mutable Heap Numbers Boost JavaScript Performance by 2.5x

By

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.

How V8's Mutable Heap Numbers Boost JavaScript Performance by 2.5x
Source: v8.dev

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 seed variable 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.random revealed two major performance issues:

  • HeapNumber allocation: Because the seed value is a double (the result of arithmetic involving fractional division), it cannot be stored as an SMI. Instead, every update to seed creates 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.

Back to top

Tags:

Related Articles

Recommended

Discover More

Ibogaine for PTSD: What Veterans' Trials Reveal About a Psychedelic TreatmentLinux Firmware Service Cuts Access for Non-Contributing Vendors Amid Sustainability CrisisBreaking: Edge Infrastructure Under Siege – Attackers Exploit Decaying Perimeter Security at Machine SpeedHow to Navigate the New Combined Coursera-Udemy Platform: A Step-by-Step GuideVera Rubin Observatory: Unveiling Cosmic Giants and Celestial Wanderers