Skip to content

docs: record the measured #7109 timing in the #7121 changelog fragment - #7125

Merged
proggeramlug merged 1 commit into
mainfrom
docs/7123-7121-timing-followup
Jul 31, 2026
Merged

docs: record the measured #7109 timing in the #7121 changelog fragment#7125
proggeramlug merged 1 commit into
mainfrom
docs/7123-7121-timing-followup

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #7121 (#7109). Documentation only — six lines in the changelog
fragment, no code.

#7121 shipped with no timing claim, because the host was at load 15–80 all
session and 08_string_concat at its committed 100 000 iterations runs in under
10 ms — below the resolution of its own Date.now() delta. Saying "we did not
measure" was the right call then; the measurement has since been made and
belongs in the release notes rather than in a PR comment.

The measurement

08_string_concat's loop, unchanged in shape, scaled to 3 000 000 iterations so
the in-program timer has something to resolve, and timed by that timer (process
startup excluded). 20 interleaved pairs — alternating the two binaries
rather than running 20 of one then 20 of the other, so a drift in host load
cannot be attributed to the change.

const ITERATIONS = 3000000;
let result = "";
const start = Date.now();
for (let i = 0; i < ITERATIONS; i++) {
    result = result + "x";
}
console.log("elapsed_ms:" + (Date.now() - start));
arm median min distribution
4d3ddc9a3 (base) 74 ms 72 ms 72–79 ms
with #7109 69 ms 68 ms 68–72 ms

−6.8 % median, −5.6 % minimum. The distributions are essentially disjoint,
which is what makes this reportable at load ≈26 — the intra-arm spread is ~9 %
of the median but the arms do not interleave.

The mechanism is visible in the IR and was already in #7121: the top-level
result = result + "x" went from two js_get_string_pointer_unified calls per
iteration to Phase 3a's four-arm tag dispatch whose hot arm derives both handles
with a bare and i64 …, 0xFFFF_FFFF_FFFF.

Why it is stated narrowly

29 of the 39 workloads in #7121's object A/B compile to a byte-identical
.o.
#7109 converted 321 module_init_context denials into selections, but
most of those are canonical-i32 promotions that -O3 was already achieving —
under the parallel-shadow model every LocalGet of such a local already read
the i32 slot, so the double slot was dead code.

So this is a number about the top-level += self-append idiom, which is the
one lowering that changes and survives optimization. It is deliberately not
presented as "#7109 is worth −6.8 %".

Verification

Both binaries built from the same runtime archives (libperry_runtime.a /
libperry_stdlib.a unchanged between the two commits — #7109 touches
perry-codegen only), PERRY_NO_AUTO_OPTIMIZE=1, --no-cache, and the two
linked binaries confirmed to differ by hash before timing, so this is not an
A/B of one binary against itself.

No code, no test, and no gate changes. changelog.d/ fragments are folded into
the release notes at tag time.

Summary by CodeRabbit

  • Performance
    • Reported a measured improvement for a targeted module initialization scenario, reducing runtime from 74 ms to 69 ms in a 3,000,000-iteration benchmark.
    • Documented timing ranges and noted that 29 of 39 tested workloads produce byte-identical results.

The fragment shipped with no timing claim because the host was too loaded to
make one at the time. Measured after merge: `08_string_concat`'s top-level
`result += "x"` scaled to 3,000,000 iterations, timed by the program's own
`Date.now()` delta over 20 INTERLEAVED pairs, goes 74 ms -> 69 ms (-6.8%) with
distributions 72-79 ms vs 68-72 ms.

Stated narrowly on purpose. 29 of the 39 workloads in the A/B compile to a
byte-identical object, so this is a number about the top-level `+=` self-append
idiom the canonical-Str lowering changes, not about the 321 selections #7109
converted.

Claude-Session: https://claude.ai/code/session_018ZFER8EEg8K7ez2n6oDrT9
@proggeramlug
proggeramlug merged commit ed6c2f4 into main Jul 31, 2026
29 of 36 checks passed
@proggeramlug
proggeramlug deleted the docs/7123-7121-timing-followup branch July 31, 2026 07:23
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 932dd45c-6534-46b7-85c4-7cc1259ea5f6

📥 Commits

Reviewing files that changed from the base of the PR and between 95988fb and d814cb6.

📒 Files selected for processing (1)
  • changelog.d/7121-module-init-repsel.md

📝 Walkthrough

Walkthrough

The changelog adds measured performance results for the module-init canonical-representation change. It records a 3,000,000-iteration improvement from 74 ms to 69 ms and notes output identity across 39 workloads.

Changes

Module-init benchmark documentation

Layer / File(s) Summary
Benchmark results
changelog.d/7121-module-init-repsel.md
Documents the benchmark method, timing distributions, observed speedup, and the 29 of 39 workloads that produce byte-identical objects.

Estimated code review effort: 1 (Trivial) | ~2 minutes

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/7123-7121-timing-followup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Independent reconciliation of this PR's number, from #7128it holds up.

Measured on a Raspberry Pi 5 at load 0.16 with perf stat -e instructions:u
(11 repeats), plus a Mac mini M1 for a wall-time sign check. Same arms as here
(4d3ddc9a3 vs #7109), built from one target dir in one session.

measurement program metric result
this PR 3M, in-program timer (startup excluded), load ≈26 wall −6.8% median
#7128, Pi, load 0.16 3M, whole process instructions −4.21%
#7128, Pi, load 0.16 3M, whole process wall −3.4%
#7128, mini, load ~4–8 3M, whole process wall −5.1%
#7128, Pi, load 0.16 committed 100k, whole process instructions −4.12%

These agree; the spread is almost entirely whether process startup is in the
denominator
. The committed 100k program is ~24 ms wall on the mini and
essentially all startup, so the mini's 3M numbers net of startup are ~51 vs
~55 ms — −7.3%, bracketing the −6.8% here. Your figure is right for
loop-only wall; −4.2% is the instruction cost actually removed. Time falls
further than instruction count because the change removes a call
(js_get_string_pointer_unified) whose cost exceeds its instruction count.

Two additions:

  1. The effect survives at the committed 100,000-iteration size — −4.12%
    instructions on the unmodified committed file, no scaling. Wall clock reads
    21 ms in both arms there, which is exactly why you needed the 30× scaling for
    a timer; instructions resolve it without changing the program's working set
    or cache behaviour. So the scaling was a legitimate technique and it did not
    manufacture the effect.
  2. It is canonical Str and nothing else — the 4d3ddc9a3 arm and a
    PERRY_CANONICAL_STR_LOCALS=0 arm of main agree to within 900 instructions
    out of 926 million.

One caveat for anyone reusing that knob: PERRY_CANONICAL_STR_LOCALS=0 is not
scoped to Str locals — it also gates nanbox_string_ref_boxed, so 24 of 26
census workloads emit differently under it, including ones with
canonical-str = 0. The compiler-arm A/B you used is the isolation that is
actually valid here.

Cost side, reported separately from speed: canonical Str adds 2,808 bytes
of Mach-O __text on 08_string_concat.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant