Boosting JavaScript Speed: How V8's Mutable Heap Numbers Turbocharged a Benchmark

By

Introduction

At the V8 engine team, our mission is to squeeze every ounce of performance out of JavaScript. In a recent deep dive into the JetStream2 benchmark suite, we identified a critical performance cliff hiding in plain sight. By rethinking how numeric values are stored in script contexts, we achieved a stunning 2.5× speedup in the async-fs benchmark—and a noticeable lift to the overall JetStream2 score. This optimization was born from a benchmark workload, but the pattern it targets appears often in real-world applications.

Boosting JavaScript Speed: How V8's Mutable Heap Numbers Turbocharged a Benchmark
Source: v8.dev

The Benchmark and Its Surprising Bottleneck

The async-fs benchmark simulates a JavaScript file system with asynchronous operations. Inside its code, we found an unexpected performance hotspot: a custom, deterministic implementation of Math.random. The implementation uses a variable called seed, updated on every call to generate a pseudo-random sequence.

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 V8 Stores Variables in Script Contexts

The seed variable lives in a special storage area called a ScriptContext. A ScriptContext is an array of V8’s tagged values, accessible from any code in the same script. On 64-bit systems, each tagged value occupies just 32 bits. The least significant bit determines the type:

This tagging allows V8 to store small integers efficiently without heap allocations. But what happens when a variable in a ScriptContext holds a non-SMI number—like the seed variable, which goes through many arithmetic operations and quickly exceeds the SMI range?

The Cost of Immutable HeapNumbers

When a variable in a ScriptContext can’t be stored as an SMI, V8 places it on the heap as a HeapNumber object. A HeapNumber is an immutable 64-bit double-precision value. The ScriptContext slot then holds a pointer to that HeapNumber.

Here’s the problem: every time Math.random runs, seed gets a new value. Because HeapNumbers are immutable, the engine must allocate a new HeapNumber on the heap each iteration. The old one becomes garbage. Profiling revealed that this allocation was the number one bottleneck in the async-fs benchmark, generating a massive number of short-lived objects and putting pressure on the garbage collector.

The Optimization: Mutable Heap Numbers

Our fix was straightforward but impactful: we introduced mutable HeapNumbers for variables stored in ScriptContexts. Instead of allocating a new object on every write, V8 now allows the existing HeapNumber’s value to be mutated directly. The pointer in the ScriptContext remains the same; only the double inside changes.

This change eliminated the allocation overhead entirely. The seed variable now lives in a single, mutable heap number throughout the benchmark’s lifetime. The garbage collector sees no new objects, and the repeated updates become simple memory writes.

Implementation Details

We modified the StoreIC (inline cache) and the baseline compiler to recognize when a store targets a mutable heap number in a script context. In that case, instead of creating a new HeapNumber, the code patches the existing value. The change touches only the V8 runtime’s handling of script-context allocations; all other numeric representations remain unchanged.

Results: A 2.5× Speedup

With mutable heap numbers enabled, the async-fs benchmark ran 2.5 times faster than before. The overall JetStream2 score increased by a few percent—a meaningful improvement for a benchmark suite that measures many diverse workloads. While the direct benefit is most visible in benchmarks that perform many numeric updates in a script context, the pattern (storing a frequently mutated number in a top-level variable) appears in real code, such as game loops, simulations, and custom PRNGs.

Real-World Impact

This optimization isn’t just for benchmarks. JavaScript developers often use pattern like let state; ... state = newState; inside modules. If state is a large number that doesn’t fit an SMI, every update would previously have created a new heap allocation. With mutable heap numbers, such code becomes faster and more memory-efficient.

Related Optimizations

We also applied similar reasoning to other context types, such as block-scoped variables in functions, where mutable heap numbers can reduce allocation pressure. Ongoing work explores extending the concept to object properties and arrays.

Conclusion

The journey to better JavaScript performance often starts by questioning assumptions. In this case, the assumption that HeapNumbers must be immutable turned out to be a performance cliff. By making them mutable for script-context slots, V8 removed a hidden allocation tax. The result is a faster engine for everyone—whether you’re running benchmarks or shipping production apps.

Tags:

Related Articles

Recommended

Discover More

10 Critical Insights Into Google’s First AI-Crafted Zero-Day Exploit That Bypasses 2FAHow the FBI Recovered Deleted Signal Messages from an iPhone's Notification CacheMastering CSS Contrast: A Complete Guide to the contrast() FilterCommemorative Steve Jobs $1 Coin: How to Get the U.S. Mint’s Latest American Innovation IssueMay Cloud Gaming Mega-Update: 10 Things to Know About GeForce NOW's Biggest Month Yet