Quick Facts
- Category: Environment & Energy
- Published: 2026-05-14 12:27:09
- Budget Laptops Under $500: Your Top Questions Answered
- Six Educators to Lead National Conversation on AI in Schools as ISTE+ASCD Announces 2026-27 Voices of Change Fellows
- The Ultimate Guide to Crafting a High-Quality Knowledge Base for AI Systems
- Streamlining Carbon Footprint Management: A Guide to AWS Sustainability Console
- Critical Zero-Day in cPanel, Medtronic Breach, and AI Tool Abuse: This Week’s Top Cyber Threats
Introduction
At V8, we are always looking for ways to make JavaScript faster. One recent optimization gave us a 2.5x speedup in the async-fs benchmark from JetStream2. The trick was to replace immutable heap numbers with mutable ones, especially in the custom Math.random implementation used by the benchmark. While this change was sparked by a benchmark, the pattern appears in real JavaScript code and can yield big performance wins. In this guide, you’ll learn step by step how to identify and fix similar bottlenecks in V8’s handling of mutable heap numbers.
What You Need
- V8 source code – access to the V8 engine codebase (open-source at chromium.googlesource.com/v8/v8).
- Familiarity with V8 internals – understanding of object representation, heap layout, and the ScriptContext.
- JavaScript knowledge – especially how
Math.randomis commonly implemented and why mutable state matters. - Build environment – a working V8 build (use
gnandninja). - JetStream2 benchmark suite – specifically the async-fs test to reproduce the performance cliff.
- Profiling tools – e.g.,
perfon Linux or V8’s built-in--traceflags.
Step 1: Understand How V8 Stores Numbers in ScriptContext
Every JavaScript ScriptContext is an array of tagged values. On 64-bit systems each slot is 32 bits. The least significant bit (LSB) indicates the type:
- 0 = a 31-bit Small Integer (SMI) – the actual integer is stored left-shifted by one bit.
- 1 = a compressed pointer to a heap object (the pointer is incremented by one).
Numbers that cannot fit as SMI (like large integers or floating-point numbers) are stored as HeapNumber objects on the heap. By default, these HeapNumbers are immutable. Once created, they cannot be changed; any assignment creates a new HeapNumber.
Step 2: Identify the Bottleneck in Math.random
In the async-fs benchmark, the custom Math.random is implemented as a closure that keeps a seed variable in the ScriptContext. The seed is updated on every call using a series of bitwise operations. Because the seed is a floating-point number (or a large integer), it cannot be stored as SMI. Consequently, each update allocates a new immutable HeapNumber, causing:
- High allocation pressure in the heap.
- Frequent garbage collection.
- Expensive pointer dereferences.
To confirm, profile the Math.random function using V8’s --trace-deopt or a sampling profiler. You’ll see many allocations and GC stalls.
Step 3: Design a Mutable Heap Number Object
The fix is to allow the HeapNumber to be mutable. Instead of allocating a new object every time, we modify the same object in place. This requires changes in V8’s runtime to support mutation of HeapNumbers. Key design points:
- The mutable HeapNumber should still be a normal JavaScript object (no syntax changes).
- Use a flag or tag to indicate mutability – for example, a special kMutableHeapNumberMap or an inline property.
- Mutation must be atomic and safe for concurrent threads (V8 uses a single-threaded execution model for JavaScript, so no locks needed).
Step 4: Implement Mutable Heap Numbers in V8’s Runtime
Edit the V8 source code to add a new MutableHeapNumber class (or extend the existing one). Modify the object allocation code to create mutable versions when needed. For the ScriptContext slot, when storing a number that will be frequently updated, replace it with a mutable HeapNumber.
In the runtime (C++ code), add a function like SetMutableHeapNumberValue(isolate, object, new_value) that writes the new double into the object’s value field without allocation. Ensure the garbage collector does not move or compact these objects (they are still movable, but their value field is updated in place).
Tip: Look at V8’s existing MutableHeapNumber used for the NaN boxed representation – you can reuse similar logic.
Step 5: Update Math.random to Use a Mutable Heap Number
Make the JIT-compiled Math.random function (or the interpreter) aware of the mutable HeapNumber. When compiling the inline cache or the load/store operations, check if the heap number is mutable. If yes, generate code that directly updates its value inline (e.g., using STORE_DOUBLE_FIELD). This avoids the allocation path.
In the IC (Inline Cache) handler for property stores, add a fast path for mutable heap numbers. Similarly, the load path can read the double directly without an allocation check.
Step 6: Verify Performance Gains
- Build V8 with the changes.
- Run the async-fs benchmark in isolation:
d8 test/benchmarks/JetStream2/async-fs.js - Compare the total score and the number of GC events before and after.
- Check for no regressions in other benchmarks (e.g., other JetStream2 tests, Octane, SunSpider).
- Use
--trace-optto confirm the optimizedMath.randomis now using mutable heap numbers.
In our implementation, this change alone gave a 2.5x speedup in the async-fs benchmark and lifted the overall JetStream2 score.
Tips and Best Practices
- Test with real-world code – look for other patterns where a number variable is updated inside a hot loop (e.g., counters, audio sample generators). Mutable heap numbers will help there too.
- Monitor memory – mutable heap numbers eliminate allocations but they still occupy heap space. Ensure they are not leaked.
- Handle deoptimizations – if a mutable heap number escapes to a generic JavaScript environment (e.g., passed to a host API), fall back to the immutable path.
- Consider Smi promotion – if the seed sometimes fits in a 31-bit Smi, you might lose the optimization. Ensure your heuristic prefers mutable heap over Smi for frequently updated values.
- Use V8’s built-in profiling tools regularly:
--trace-gc,--trace-ic, and the built-in%DebugPrintto inspect object types.