Boosting V8 Performance: How Mutable Heap Numbers Eliminate Allocation Bottlenecks

By — min read

Introduction

At V8, we continuously refine JavaScript performance to meet the demands of modern web applications. Recently, we turned our attention to the JetStream2 benchmark suite, aiming to eliminate performance cliffs. This article details a specific optimization—replacing immutable heap numbers with mutable ones—that yielded a remarkable 2.5× improvement in the async-fs benchmark, significantly boosting the overall score. While inspired by this benchmark, the optimization targets patterns that appear in real-world code.

Boosting V8 Performance: How Mutable Heap Numbers Eliminate Allocation Bottlenecks
Source: v8.dev

Inside the async-fs Benchmark

The async-fs benchmark is a JavaScript file system implementation focused on asynchronous operations. Surprisingly, its main bottleneck turned out to be a custom, deterministic implementation of Math.random. The implementation uses 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;
  };
})();

The seed variable resides in a ScriptContext, which acts as a storage container for values accessible within a script. Internally, the ScriptContext is an array of V8's tagged values. On 64-bit systems with default configuration, each tagged value occupies 32 bits. The least significant bit acts as a tag:

  • 0: a 31-bit Small Integer (SMI)—the actual integer is stored directly, left-shifted by one bit.
  • 1: a compressed pointer to a heap object (the pointer value is incremented by one).

This tagging system efficiently handles different numeric types. SMIs are stored inline. Larger numbers or numbers with decimal parts are stored indirectly as immutable HeapNumber objects on the heap—a 64-bit double—with the ScriptContext holding a compressed pointer to them.

The Bottleneck: HeapNumber Allocation

Profiling Math.random revealed two major performance issues:

  1. HeapNumber allocation: The slot for seed in the ScriptContext points to a standard, immutable HeapNumber. Each time the function updates seed, a new HeapNumber must be allocated on the heap, causing significant allocation overhead.
  2. The allocation occurs repeatedly in a tight loop, overwhelming the garbage collector and degrading performance.

Even though the seed value fits in a 32-bit signed integer after masking, V8 stores it as a HeapNumber because the initial assignment and updates produce values that exceed the SMI range during intermediate calculations. Once an object becomes a HeapNumber, it remains one, and each subsequent write to seed triggers a new allocation.

The Optimization: Mutable Heap Numbers

To eliminate these allocations, we introduced the concept of mutable heap numbers. The idea is simple: allow a HeapNumber object to be mutated in-place, rather than requiring a new object for every update. When V8 detects a pattern where a variable in a ScriptContext (or other context) is repeatedly assigned new numbers that do not fit in an SMI, it can promote the existing HeapNumber to a mutable state.

With mutable heap numbers, the update to seed no longer allocates a new HeapNumber. Instead, the double value stored inside the existing HeapNumber is overwritten directly. This significantly reduces memory allocation and garbage collection pressure.

Results and Impact

The optimization yielded a 2.5× speedup in the async-fs benchmark. This improvement contributed noticeably to the overall JetStream2 score. The fix is particularly effective because Math.random is called in a hot loop, and the custom implementation's six-step seed update sequence amplifies the allocation cost when each step must create a new HeapNumber.

Real-World Relevance

While the immediate trigger was a benchmark, the pattern of storing mutable numeric state in script or function contexts appears in real production code. Examples include counters, accumulators, custom random number generators, incremental hashes, and state machines. Eliminating allocation cliffs for such variables can improve performance in data processing pipelines, simulations, and game loops.

Technical Implementation Details

V8's existing object model already supports mutation for other object types (e.g., JavaScript objects). Heap numbers were originally immutable to simplify optimization and memory management. The key change involved:

  • Adding a flag to HeapNumber objects to indicate mutability.
  • Modifying the StoreIC (inline cache) for property stores to detect when the target is a mutable HeapNumber and perform an in-place write instead of allocating.
  • Ensuring correct behavior under concurrent garbage collection and deoptimization.

The change is safe because the new value is of the same type (double). If the variable later transitions to an SMI, V8 can revert to storing it directly in the context slot. The mutable HeapNumber is only used when frequent numeric updates are observed.

Summary

By replacing immutable HeapNumber allocations with mutable in-place updates, V8 eliminated a major performance bottleneck in the async-fs benchmark. The resulting 2.5× improvement shows how targeted optimizations based on real usage patterns can yield substantial gains. Developers can benefit from this enhancement transparently, without changing their code.

For further reading, see the bottleneck analysis and results sections above.

Tags:

Recommended

Discover More

Python 3.14.3 and 3.13.12 Deploy Critical Bug Fixes and New Features10 Key Insights on the Adobe-NVIDIA-WPP AI Agent Revolution for MarketingMedicare's RAPID Pathway: A Leap Forward for Adults, But Children Remain in the Queue10 Critical Red Flags for Websites with Undefined Trust: A Guide to Online SafetyCloudflare Restructures for the AI Era: Workforce Reduction and Strategic Shift