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
% in crates/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>/release
The 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 RSS
Profiling: 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
Summary
pipelinespends 7.3% of self time in libmfmod. It currently runs at 1.26x Nodewith an instruction ratio of 1.88.
Profile evidence
Measured on
8bfdac447, symbols kept, aggregated over 14 runs:Why this is worth fixing
A
%on two doubles is being lowered to a libmfmodcall. For the overwhelmingly commoncase — 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
fmodotherwise. 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% oninterpand−5.4% on
iso_miss. The same "cheap runtime check before the general path" pattern applieshere.
Care needed on semantics — JavaScript
%is truncated remainder, not Euclidean modulo,and must preserve the sign of the dividend.
-7 % 3is-1, not2. NaN, infinities, anda 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
%incrates/perry-codegen/src/expr/fmodAlso visible in the same profile
js_implicit_this_setat 4.9% may be worth a separate look — it is called on a hot methodpath. Note it also appeared in #8243's investigation as a default-off unmeasured suspect.
Acceptance
pipelinemeasurably improved (ideally < 1.00x Node),%semantics unchanged under adifferential 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:
The benchmark corpus and Node-oracle expected output are staged at
/Users/amlug/projects/perry/sweep-artifacts-0819b/(sources/*.ts,expected/*.stdout).Profiling: Perry strips its output binaries, so
sampleattributes everything to???. SetPERRY_KEEP_SYMBOLS=1 PERRY_DEBUG_SYMBOLS=1at 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
minimize RSS and keep best compute — never trade one for the other silently.
cargo test --release -p perry-runtime --lib,-p perry --bin perry, andbash scripts/run_lint_gates.sh(all 50) must pass.obviously guilty on this exact corpus and measured +0.2%.)
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.