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_LAYOUT — one 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.
bench_array_opsis 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
sampleover a--debug-symbolsbuild:perry_fn_…__runArrayBenchmark(generated)js_typed_feedback_numeric_array_index_set_guardjs_array_fill_f64_iota_extendjs_array_growjs_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_guarddoes the real work: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 ani32loop counter(*header)._reserved & GC_ARRAY_RAW_F64_LAYOUT— one flag-bit test on the GC headerEvery 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) andjs_array_fill_f64_iota_extend(89) are the growth path forarr[i] = ion 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.