V8's Mutable Heap Numbers: A 2.5x Speed Boost for JavaScript

By ● min read

Introduction

At V8, performance is an ongoing mission. Recently, the team targeted the JetStream2 benchmark suite to identify and remove performance bottlenecks. One optimization, inspired by the async-fs benchmark, resulted in a remarkable 2.5x improvement for that specific test and a noticeable lift in the overall score. While the benchmark triggered the change, similar patterns appear in real-world JavaScript code.

V8's Mutable Heap Numbers: A 2.5x Speed Boost for JavaScript
Source: v8.dev

The async-fs Benchmark and Its Math.random

The async-fs benchmark implements a JavaScript file system with asynchronous operations. Surprisingly, its main performance culprit was the custom Math.random implementation. To guarantee reproducible results, the benchmark uses a deterministic pseudo-random number generator:

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, generating the pseudo‑random sequence. Critically, seed is stored in a ScriptContext, a storage area for variables accessible within a script.

Understanding ScriptContext and Number Storage

Internally, V8 represents a ScriptContext as an array of tagged values. On 64‑bit systems (default configuration), each tagged value occupies 32 bits. The least significant bit acts as a tag:

This tagging scheme determines how numbers are stored:

The ScriptContext layout includes slots for metadata, the global object (NativeContext), and variables like seed. An untagged double‑precision value occupies its own slot. This design efficiently handles various numeric types while optimizing for the common SMI case.

The Performance Bottleneck

Profiling the custom Math.random exposed two major issues:

  1. HeapNumber allocation: The slot for the seed variable points to an immutable HeapNumber. Every time Math.random updates seed, a new HeapNumber object must be allocated on the heap. This allocation cost adds up quickly, especially when Math.random is called frequently.
  2. No in-place mutation: Because HeapNumbers are immutable, updating seed cannot happen in place. The engine must create a fresh object and update the pointer, causing memory management overhead.

In the async-fs benchmark, the heavy use of Math.random for file system operations (e.g., generating unique IDs) made this bottleneck significant.

The Optimization: Mutable Heap Numbers

The V8 team introduced a new mechanism: mutable heap numbers. Instead of replacing the entire HeapNumber when seed changes, the engine now allows the existing HeapNumber object to be mutated in place—its double value can be overwritten. This eliminates the allocation and garbage collection pressure.

The key changes include:

This optimization was inspired by the benchmark but naturally applies to any hot code that repeatedly updates a numeric variable in a context, such as counters, accumulators, or generators.

Impact and Conclusion

After implementing mutable heap numbers, the async-fs benchmark showed a 2.5x speedup. The overall JetStream2 score also improved noticeably. This case illustrates how profiling pinpointed a seemingly small issue—HeapNumber allocation in a script context—that caused a large performance cliff.

V8’s approach to mutable heap numbers demonstrates a pragmatic tradeoff: sacrificing immutability in a strictly controlled internal case to gain runtime efficiency. Such optimizations, while triggered by benchmarks, often translate to real‑world benefits for JavaScript applications that perform heavy numeric computations.

For more details on V8’s performance improvements, see related articles on number storage and bottleneck analysis.

Tags:

Recommended

Discover More

10 Key Facts About Fedora Atomic Desktop Sealed Bootable ImagesHow to Upgrade Your .NET WASM App from .NET 8 to .NET 10FDA Greenlights Axsome's Breakthrough Treatment for Alzheimer's AgitationNavigating the Federated Social Web: Your Guide to Linking Mastodon, Bluesky, and BeyondMastering Python Fundamentals: A Structured Approach to Conceptual Clarity