Skip to content

perf: array element store pays a cross-crate guard call per element (15% of bench_array_ops) #7396

Description

@proggeramlug

bench_array_ops is 4.5× slower than Node (262 vs 58 by the benchmark's own timer), and more than half the time is outside the generated code.

Measured

sample over a --debug-symbols build:

samples frame
326 perry_fn_…__runArrayBenchmark (generated)
103 js_typed_feedback_numeric_array_index_set_guard
89 js_array_fill_f64_iota_extend
87 js_array_grow
52 js_gc_temp_root_{push,get,truncate}

The workload is pure integer array traffic — arr[i] = i, sum += arr[i], a swap loop, arr[i] % 2.

What the guard actually is

Not overhead for a disabled feature — I assumed that first and it is wrong. With typed feedback off (the default) js_typed_feedback_numeric_array_index_set_guard does the real work:

if !typed_feedback_enabled() {
    return (index >= 0
        && is_numeric_value_bits(value.to_bits())
        && numeric_array_index_set_guard(arr, index, require_in_bounds)) as i32;
}

It is the check that authorises the fast store. The problem is where it lives: a 5-argument cross-crate call per element, for a predicate whose common case is

  • index >= 0 — statically known for an i32 loop counter
  • value is numeric — often statically known
  • plain array + in bounds — one length load and compare
  • (*header)._reserved & GC_ARRAY_RAW_F64_LAYOUTone flag-bit test on the GC header

Every one of those is a handful of inline instructions. This is the "native-able primitive became a runtime call" pattern the earlier audits named, in its clearest form.

Suggested direction

Emit the fast-path predicate inline in codegen and keep the helper as the slow-path fallback, so a store that stays on the raw-f64 layout never leaves the generated function. The layout flag test is the load-bearing part and is a single masked load.

js_array_grow (87) and js_array_fill_f64_iota_extend (89) are the growth path for arr[i] = i on an initially-empty array and are worth a separate look — Node pre-sizes far more aggressively.

Caveat on the measurement

Taken on a machine at load ~5.9, using minimum-of-N and the benchmark's own internal timer rather than wall clock. The sample proportions are stable across runs but the absolute ratio should be re-confirmed on a quiet host before it anchors any target.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions