Skip to content

codegen: proven-Number f64 locals stay nanbox-rooted (not raw double) in functions that read typed arrays — ~4x f64-vs-i32 gap #8619

Description

@proggeramlug

Summary

In a function that reads a typed array, numeric f64 locals — including a fresh, non-loop-carried one — are stored as nanbox GC roots (with a per-write js_write_barrier_root_nanbox) instead of raw doubles, even when their value is a proven Number. This is measurable: a dense Float64Array reduction runs ~4× slower than the equivalent Int32Array one, and the gap is the f64 accumulator's rooting + write-barrier overhead (the i32 accumulator stays native and unrooted).

Surfaced while inlining typed-array reads (#8617). That PR removed the per-read runtime call (Float64Array 3.60s → 1.73s); the remaining ~1.73s vs the i32 path's 0.42s is this rooting overhead.

Reproducer

let arr = new Float64Array(4096);
for (let i = 0; i < 4096; i++) { arr[i] = i * 0.5; }
function hot() {
  let s = 0.0;
  for (let i = 0; i < 4096; i++) {
    let x = arr[i] + 1.0;   // fresh, proven Number
    s = s + x;
  }
  return s;
}
console.log("s:" + hot());

PERRY_SAVE_LL on hot: 0 alloca double, 4 alloca ptr addrspace(1) (nanbox), 7 js_write_barrier_root_nanbox, 0 fadd. Both x and s are nanbox-rooted; the loop compare i < 4096 also round-trips through f64 (sitofp + fcmp + nanbox-bool + icmp eq) instead of a native icmp slt i32.

For comparison, a pure-numeric loop with no typed-array in the function keeps some alloca double + fadd. So the typed-array involvement broadly disables the raw-double representation for the whole function — even for locals unrelated to the array.

What to investigate

  1. Why representation selection (repsel / numeric-envelope) declines the raw-double slot for a proven-Number local in a function that reads a typed array — is a conservative flag set function-wide?
  2. The loop-carried accumulator s = s + <number> needs a fixpoint proof to keep its raw-double representation across the back-edge.
  3. The i32 loop-condition (i < const) should lower to a native icmp slt i32, not the f64 round-trip. (LLVM may already fold the sitofp+fcmp; worth confirming against the optimized asm before treating it as a runtime cost.)

Caveat

Some of the pre-optimization nanbox/round-trip IR is cleaned by LLVM at -Os/-O3, and the write barrier is runtime-guarded (a load+branch, skipped when the incremental marker is inactive). The measured 4× f64-vs-i32 gap says a real residual cost remains, but the exact split (rooting vs barrier vs compare) should be confirmed by profiling the optimized binary. Related: #5497 (type-directed specialization), #8617.

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions