Repository navigation
perf: JS % lowers to a libm fmod call — 7.3% of pipeline self time #8407
Description
Summary
pipeline spends 7.3% of self time in libm fmod. It currently runs at 1.26x Node
with an instruction ratio of 1.88.
Profile evidence
Measured on 8bfdac447, symbols kept, aggregated over 14 runs:
28.7% perry_method_pipeline_ts__u_Registry_$_fn_num__get$pshape <- user code
25.4% perry_method_pipeline_ts__u_Registry_$_fn_num__set$pshape <- user code
16.8% perry_fn_pipeline_ts__main <- user code
7.3% fmod <- THIS
5.5% perry_method_pipeline_ts__u_Registry_$_str_num__get$pshape
4.9% js_implicit_this_set
3.7% js_closure_call1
1.5% js_string_concat_box
1.5% gc::layout_tables::layout_forget_object
Why this is worth fixing
A % on two doubles is being lowered to a libm fmod call. For the overwhelmingly common
case — both operands are integral and in a range where the result is exact — this can be a
handful of inline instructions instead of a function call into libm.
V8 does exactly this: it emits an inline integer modulo when it can prove or cheaply check
integrality, and falls back to fmod otherwise. The check is cheap relative to the call.
There is precedent in this codebase for the shape of the fix: #8395 gave the relational
operators the numeric early-out that + and === already had, worth −7.0% on interp and
−5.4% on iso_miss. The same "cheap runtime check before the general path" pattern applies
here.
Care needed on semantics — JavaScript % is truncated remainder, not Euclidean modulo,
and must preserve the sign of the dividend. -7 % 3 is -1, not 2. NaN, infinities, and
a zero divisor must all keep their current behaviour. A differential test against Node over
a spread of operand kinds (integral, fractional, negative, ±0, ±Infinity, NaN, very large
magnitudes where doubles lose integer precision) is the right way to pin that.
Where to look
- the binary-op lowering for
%incrates/perry-codegen/src/expr/ - whatever runtime helper currently reaches
fmod
Also visible in the same profile
js_implicit_this_set at 4.9% may be worth a separate look — it is called on a hot method
path. Note it also appeared in #8243's investigation as a default-off unmeasured suspect.
Acceptance
pipeline measurably improved (ideally < 1.00x Node), % semantics unchanged under a
differential test against Node, all 19 rows byte-exact, RSS not increased.
How to build and measure (shared setup)
Build with the static wrappers or you link a stale runtime and both A/B arms behave
identically — a vacuous result:
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
export PERRY_RUNTIME_DIR=<your target>/releaseThe benchmark corpus and Node-oracle expected output are staged at
/Users/amlug/projects/perry/sweep-artifacts-0819b/ (sources/*.ts, expected/*.stdout).
export PERRY_NO_AUTO_OPTIMIZE=1 PERRY_NO_CACHE=1
perry sources/<row>.ts -o /tmp/x --no-cache
cmp <(/tmp/x) expected/<row>.stdout # verify BEFORE timing
/usr/bin/time -l /tmp/x # instructions retired + peak RSSProfiling: Perry strips its output binaries, so sample attributes everything to
???. Set PERRY_KEEP_SYMBOLS=1 PERRY_DEBUG_SYMBOLS=1 at compile time — the binary keeps
~1900 symbols and stays byte-exact — then read sample's "Sort by top of stack" section.
These programs are short; aggregate across ~14 runs for a usable sample count.
Constraints that apply to any fix here
- Correctness first: all 19 corpus programs must stay byte-exact against their Node oracle.
- Report wall time AND instructions retired AND peak RSS together. The project rule is
minimize RSS and keep best compute — never trade one for the other silently. cargo test --release -p perry-runtime --lib,-p perry --bin perry, and
bash scripts/run_lint_gates.sh(all 50) must pass.- A diff-read is a hypothesis, not a result — measure before proposing. (fix(async_hooks): track scheduler resource lifecycles #8328 looked
obviously guilty on this exact corpus and measured +0.2%.) - Known unrelated flake:
box::release_tests::the_intrusive_free_list_round_trips_a_whole_cohort
(flaky: the_intrusive_free_list_round_trips_a_whole_cohort asserts a delta on a process-global counter under parallel tests #8401). If it fails once and reruns clean, it is not yours.
Activity