O.putty PDocsEnvironment & Energy
Related
Rising Tensions: 10 Key Insights Into Solar Project Complaints and Community RelationsNavigating Away from Sea of Nodes: A Guide to V8's Transition to TurboshaftElectric Vehicle Sales Soar: IEA Predicts 23 Million EVs by 20266 Game-Changing Insights from V8's Mutable Heap Numbers OptimizationHow to Secure Defense Department Approval for Wind Farm Projects Amid New National Security StallsHarnessing Zeros: How Sparse Computing Could Revolutionize AI EfficiencyPioneering Wind-Battery Project Secures First Community Benefits Deal Under New State Planning RulesQ&A: Windows 11 Taskbar, RISC-V Router, and E Ink Color Dev Kit Explained

Boosting V8 Performance: Optimizing Mutable Heap Numbers in JavaScript Engines

Last updated: 2026-05-21 00:20:37 · Environment & Energy

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.

Boosting V8 Performance: Optimizing Mutable Heap Numbers in JavaScript Engines
Source: v8.dev

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:

  1. HeapNumber allocation: Since seed holds 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 to Math.random computes a new value for seed, 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.
  2. 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.