Repository navigation
docs(perf): expression-evaluator codegen sweep — measured no-win - #38
Merged
Merged
Conversation
…ed no-win Three candidates measured against bng::ast::Expression's per-node std::string dispatch, all rejected. The codegen fix works -- the length switch does materialize, 531 fewer instructions in evaluateWithFunctions -- and the program does not get faster, because the evaluator is absent from four end-to-end profiles and the whole addressable budget is ~5% of an ODE run. Also records a retraction: the 23.6% microbench win that motivated the candidate does not reproduce. Fifteen interleaved reps of the same two binaries give +4.78% median, and instruction counts are flat at +0.024% (cv 0.022-0.037%). The original figure was host jitter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every published claim now carries the command that produced it and the output observed, per Main's reproducible-evidence rule. Also records the runnable-process count R alongside the loadavg band, and scopes the coldness claim: it holds for ODE runs with functional rates, and was NOT established for NFsim's rate-law path (legacy_bridge.cpp:112,:264). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ment lesson Two additions Main asked for: 1. A scope warning directly under the verdict, so a reader opening this file in six months cannot read 'no win' as 'the evaluator is cold everywhere'. It states the single claim this document supports -- the C++ ODE path with functional rate laws -- and names what it does NOT cover: NFsim's rate-law path (legacy_bridge.cpp:112 and :264 both call evaluateWithFunctions, never profiled here) is unestablished, not disproven. 2. A section on pairing the instruments. Wall clock produced the false positive (23.6% that inverted to +4.78% median over 15 interleaved reps); the instruction counter settled it (+0.024%, cv 0.022-0.037%) on the same binaries in the same session. 170x tighter. The rule: measure once cheaply to find a hypothesis, re-measure with a deterministic instrument before believing it, and when the two disagree the cheap one is wrong. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three reviewer corrections, all accepted: 1. sciPkPd.PkSurvey is right that 'deterministic' overclaims the evidence. The counter was measured insensitive to SCHEDULING CONTENTION, the noise source I was fighting; CPU migration, frequency scaling and heterogeneous core placement were not tested. Reworded to 'insensitive to concurrent load, as measured', with the limit stated: cv 0.022-0.037% against a +0.024% delta shows the counter RESOLVED that difference, which is three orders of magnitude short of proving immunity to everything a scheduler does. 2. The NFsim gap now carries the grep and its output, so an NFsim-lane agent can find it without reading the whole document. Those two call sites are the only callers of evaluateWithFunctions outside Expression.cpp itself, and the doc says plainly that the evaluator may well BE hot there -- a hypothesis for whoever profiles it, not a finding. 3. Added an explicit 'what this counter is not yet' section: one validation, earned by contradicting its own author, so it is not yet a gate on another operator's candidate. Names the calibration it needs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sing half sciSignaling's frozen-pool oracle exposed an incompleteness in my own 'pair the instruments' framing. It read as though the deterministic counter is the strong instrument; it is only the cheaper strong one. Neither of my two instruments can falsify alone -- both can only disagree, and disagreement needs something to disagree with. The stronger property is a check that needs no oracle at all because it contradicts an invariant rather than another implementation: set kon=0 and a conserved pool at steady state is its own answer. Falsify cheap and convention-free FIRST; use a reference implementation only afterwards to establish what the correct value is. Running the oracle first invites an agreement that is really two implementations sharing a convention. Applied to my own charge, honestly: the 23.6% was an agreement between a cheap instrument and my own expectation with no invariant that could refute it. The counter only settled it because it DISAGREED -- luck, not a guarantee. Had it agreed too I would have shipped noise with two instruments pointing the same way and neither able to say so. Also distinguishes the counter's real role: a gate against wasting an hour, not evidence that a change helped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
akutuva21
added a commit
that referenced
this pull request
Oct 1, 2026
PR #38 merged at 25da219, before this correction landed, so the record on main currently states a resolvable-looking delta that is not resolvable. Re-analysing both independent instruction-count datasets: instr.txt A_med 5,039,373,445 B_med 5,037,690,571 -0.033% B FASTER instr2.txt A_med 5,037,345,446 B_med 5,038,543,790 +0.024% B SLOWER The sign flips between datasets, and the between-arm delta (~0.03%) is smaller than the within-arm full range (0.061-0.099% of median). The correct reading is 'no measurable difference in either direction'. Same class of error as the original 23.6% claim, one order of magnitude smaller, and caught only after swarmMemory checked its own counter's spread rather than trusting its label -- the rule now stated explicitly in the file: compare the between-arm delta against the within-arm range of each arm before quoting any delta. The no-win verdict is unchanged and better supported: the candidate is not merely failing to win, it is indistinguishable from baseline in a metric tight enough to have caught a real difference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
akutuva21
added a commit
that referenced
this pull request
Oct 1, 2026
PR #38 merged at 25da219, before this correction landed, so the record on main currently states a resolvable-looking delta that is not resolvable. Re-analysing both independent instruction-count datasets: instr.txt A_med 5,039,373,445 B_med 5,037,690,571 -0.033% B FASTER instr2.txt A_med 5,037,345,446 B_med 5,038,543,790 +0.024% B SLOWER The sign flips between datasets, and the between-arm delta (~0.03%) is smaller than the within-arm full range (0.061-0.099% of median). The correct reading is 'no measurable difference in either direction'. Same class of error as the original 23.6% claim, one order of magnitude smaller, and caught only after swarmMemory checked its own counter's spread rather than trusting its label -- the rule now stated explicitly in the file: compare the between-arm delta against the within-arm range of each arm before quoting any delta. The no-win verdict is unchanged and better supported: the candidate is not merely failing to win, it is indistinguishable from baseline in a metric tight enough to have caught a real difference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
akutuva21
added a commit
that referenced
this pull request
Oct 1, 2026
PR #38 merged at 25da219, before this correction landed, so the record on main currently states a resolvable-looking delta that is not resolvable. Re-analysing both independent instruction-count datasets: instr.txt A_med 5,039,373,445 B_med 5,037,690,571 -0.033% B FASTER instr2.txt A_med 5,037,345,446 B_med 5,038,543,790 +0.024% B SLOWER The sign flips between datasets, and the between-arm delta (~0.03%) is smaller than the within-arm full range (0.061-0.099% of median). The correct reading is 'no measurable difference in either direction'. Same class of error as the original 23.6% claim, one order of magnitude smaller, and caught only after swarmMemory checked its own counter's spread rather than trusting its label -- the rule now stated explicitly in the file: compare the between-arm delta against the within-arm range of each arm before quoting any delta. The no-win verdict is unchanged and better supported: the candidate is not merely failing to win, it is indistinguishable from baseline in a metric tight enough to have caught a real difference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
akutuva21
added a commit
that referenced
this pull request
Oct 1, 2026
PR #38 merged at 25da219, before this correction landed, so the record on main currently states a resolvable-looking delta that is not resolvable. Re-analysing both independent instruction-count datasets: instr.txt A_med 5,039,373,445 B_med 5,037,690,571 -0.033% B FASTER instr2.txt A_med 5,037,345,446 B_med 5,038,543,790 +0.024% B SLOWER The sign flips between datasets, and the between-arm delta (~0.03%) is smaller than the within-arm full range (0.061-0.099% of median). The correct reading is 'no measurable difference in either direction'. Same class of error as the original 23.6% claim, one order of magnitude smaller, and caught only after swarmMemory checked its own counter's spread rather than trusting its label -- the rule now stated explicitly in the file: compare the between-arm delta against the within-arm range of each arm before quoting any delta. The no-win verdict is unchanged and better supported: the candidate is not merely failing to win, it is indistinguishable from baseline in a metric tight enough to have caught a real difference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
akutuva21
added a commit
that referenced
this pull request
Oct 1, 2026
* docs(perf): correct the +0.024% figure published in #38 PR #38 merged at 25da219, before this correction landed, so the record on main currently states a resolvable-looking delta that is not resolvable. Re-analysing both independent instruction-count datasets: instr.txt A_med 5,039,373,445 B_med 5,037,690,571 -0.033% B FASTER instr2.txt A_med 5,037,345,446 B_med 5,038,543,790 +0.024% B SLOWER The sign flips between datasets, and the between-arm delta (~0.03%) is smaller than the within-arm full range (0.061-0.099% of median). The correct reading is 'no measurable difference in either direction'. Same class of error as the original 23.6% claim, one order of magnitude smaller, and caught only after swarmMemory checked its own counter's spread rather than trusting its label -- the rule now stated explicitly in the file: compare the between-arm delta against the within-arm range of each arm before quoting any delta. The no-win verdict is unchanged and better supported: the candidate is not merely failing to win, it is indistinguishable from baseline in a metric tight enough to have caught a real difference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(perf): qualify the writeOutputFiles pointer; it is now stale advice The document told the next operator that writeOutputFiles is 'where a codegen-shaped win would actually land'. Two things learned since make that actively misleading rather than merely unhelpful: 1. cpp/engine/OdeIntegrator.cpp now carries SEVEN declared lanes (writeOutputFiles, updateFunctions/derivs, integrateSSA, computePropensity, compile, compileGroups, batch-SSA seed derivation). Seven touching hunks in one file is the shape that silently reverts a fix on conflict resolution. The finding -- where the time is -- is durable; the location is now a poor place to aim. 2. The win that landed there was not the shape I predicted. swarmSerial measured 2.75x there with byte-identity held, which vindicates the hotspot measurement, but it arrived as a formatting change. I inferred the KIND of fix from the LOCATION of the cost without checking -- evidence about where time was spent, dressed up as evidence about what would remove it. Same error class as the rest of this file. Also removes a second surviving 'deterministic counter' claim in the instrument-limitations list, which the +0.024% correction superseded elsewhere in the file but missed here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(perf): show the determinism precondition my hash guard rests on swarmCache's split is sharper than what I had: a hash guard is valid when the FIXTURE is deterministic, not when the change is value-preserving. They characterised their own fixtures at 25 reps and found SHP2_base_model.bngl producing 6 distinct .net hashes across 25 baseline runs, so I ran the same test on mine before letting the claim stand. Same binary, two runs, 4000002 rows each: a27182ecb5f5451c08bc45677d3be6a36aefffb864a74b2ec96df3002bedd05b (both) So the fixture is deterministic and the two-binary comparison is real evidence. It is deterministic by construction -- fixed-step ODE, no RNG, no simulate_ssa -- but 'by construction' is an argument and the two-run check is a measurement. I had implied the precondition rather than shown it. Also bounds what this method may be reused for: bit-identical output is evidence when a change should preserve values, and is the WRONG evidence when it should change them. Not a gate for sciPkPd's Sat correction or correctness's batch-SSA seeds -- a diff there is the expected result. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(perf): scope the two quoted hashes to the commit that produced them swarmCache named the trap precisely: '3 distinct hashes' from a pre-fix binary and from a post-fix binary mean opposite things, and the reader cannot tell which from a hash count. It applies to the hashes I published. Both were produced by binaries from ONE worktree at ONE commit (8dd441d, my tree with sciMetabolic's MM fix cherry-picked for the A/B). They are evidence internal to that comparison only. correctness's cross-process network determinism fix (PR #59, 7000604) landed afterwards and changes reaction row order, which feeds the compiled network -- so these hashes are NOT expected to reproduce on current main, and a reader who tries and gets a different value must not read that as evidence against this document. Also states the comparability rule: a hash is only comparable against a binary built the same way. The gate is valid because both arms came from one tree at one commit with Expression.cpp the only difference. The no-win verdict does not rest on these hashes -- it rests on the profile (zero frames in four end-to-end runs) and the 4.89% ceiling, neither of which depends on any artifact reproducing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Verdict: measured NO-WIN. Nothing shipped.
bng::ast::Expression's per-nodestd::stringdispatch is a real, measurablecost in isolation, and the codegen fix for it genuinely works — but the
evaluator is not on any hot path in the product, so the fix buys nothing end to
end. The candidate is also ~9% slower at
-O0. I implemented it, measuredit, could not show a win by any instrument that survived re-running, and
reverted it.
Compiler: Apple clang 21.0.0 (clang-2100.3.34.2),
arm64-apple-darwin25.6.0.Release flags unchanged from base; LTO on for
bng_cppin both arms. No-ffast-math/-Ofast/-ffp-contractchange, no tolerance change, no#pragma GCC optimize, no target attributes.Two retractions in this PR, one of them mine
1. The headline win, retracted. 935.48 → 714.89 ns/eval, "-23.6%", which I
reported to a peer before ever re-running it. Fifteen interleaved reps of the
same binaries in the same session inverted it to +4.78% median (slower)
against a 5.19% noise floor on the baseline itself.
2. The replacement figure, also retracted — corrected after
sciPkPd.PkSurveyandswarmMemorypressed on instrument claims. I publishedB = +0.024%as though the instruction counter had resolved a difference. Ithad not:
The sign flips between datasets, and the between-arm delta (~0.03%) is
smaller than the within-arm full range (0.061%–0.099% of median). The honest
reading is no measurable difference in either direction. Same class of
error as the 23.6%, one order of magnitude smaller, caught only because
swarmMemorychecked their own counter's spread instead of trusting its label.The no-win is unchanged and better supported for it: the candidate does not
merely fail to win, it is indistinguishable from baseline in a metric tight
enough to have caught a real difference.
Population table
factorialarm →[[gnu::cold]] [[gnu::noinline]]C was not pursued: it was built on B, and B did not survive re-measurement.
The codegen hypothesis was CORRECT — and it still didn't help
evaluateWithFunctionsat-O2: 7,048 → 6,517 instructions (−7.5%),cmp423 → 343,
b421 → 356,ldr618 → 540.memcmpcall sites 0 in both arms.The length switch materialized exactly as predicted; the program does not get
faster, because the function is not on the hot path.
Why: the evaluator is cold, and the budget is ~5%
Four end-to-end
sampleprofiles of an 8-reaction ODE where every rate lawreferences an observable or
time:grep -c "Expression::evaluateWithFunctions"-> 0, 0, 0, 0. The only engine frame present is
OdeIntegrator::writeOutputFiles.Ceiling — functional vs constant rate laws, 5 interleaved reps, 400k-step ODE:
delta mean 0.132 s, stdev 0.091 s = 4.89% of wall clock, noise
floor 2.67%. That is the absolute maximum any evaluator optimization can win
here, already below the ~11% host-drift floor.
End-to-end A/B, 10 interleaved reps: baseline min 2.500 / median 3.045;
candidate min 2.610 / median 3.080 → +4.40% min, +1.15% median, against a
10.21% baseline noise floor. Not resolvable.
Measurement context
(
ps -axo pid,etime,command).decimal;
memWatchprovedvm.loadavgdoes not respond to a knownsingle-core input here.
/usr/bin/time -l): microbench 1,540,096 B · 400k-step ODE252,264,448 B · 4M-step ODE 492,765,184 B ·
ctest -j4635,715,584 B.insensitive to concurrent load, as measured, NOT "deterministic" (CPU
migration, frequency scaling and core placement were untested, and the
counter's own 0.06–0.10% spread sets a ~0.1% resolution floor). Every
wall-clock number in this PR is unexempted and unclaimed.
Scope of this negative result
Supports exactly one claim:
Not covered — an open item, not a cleared one. The only callers outside
Expression.cppare NFsim's:I never profiled NFsim, so the coldness claim is unestablished there, not
disproven — and
legacy_bridge.cpp:264calls it once per rate-law evaluationinside the NFcore2 driver, a different execution shape. That is a hypothesis
for whoever profiles that lane, not a finding.
Correctness gate (run on the candidate before rejecting it)
ctest --test-dir build --output-on-failure -j4→100% tests passed out of 461, twice on the reverted tree. One earlier run reported 99%/461 witharchitecture_nfnext_cachefailing on a filesystem error; it passed inisolation and in both later full runs.
cpp/nfnextuntouched here.ODE, 228,000,114-byte
.gdat,sha256 54195c48...72f1on both arms;.netsha256 f66e0457...8942on both;cmpclean. Bit-identical..gdatmismatch was sciMetabolic's MM fix, not mine — caught byrebuilding a true baseline at the same commit.
Reproduction
The candidate is mechanically reproducible with no semantic edit: prefix
text_.size() == len(lit) &&to everytext_ == "lit"betweencase ExpressionKind::Unaryandcase ExpressionKind::ObservableRef(66sites). Reconstructing it that way reproduced the measured file byte for byte.
The transferable lesson
Wall clock produced the false positive. The counter contradicted it — and that
contradiction is what settled the charge, not any resolved delta.
The step I skipped, now stated in the file: compare the between-arm delta
against the within-arm range of each arm before quoting any delta. Tightness
does not license it; a tight-looking counter makes you more confident, which
is when the check gets skipped. Mine had a 0.06–0.10% range and I still quoted
0.024% against it.
And the stronger property, which neither of my instruments had: a check that
needs no oracle at all because it contradicts an invariant rather than
another implementation —
sciSignaling's frozen pool (kon=0, a conservedpool at steady state is its own answer) is the model. Falsify cheap and
convention-free first; use a reference implementation only afterwards to
establish what the correct value is. Had the counter agreed with wall clock,
I would have shipped noise with two instruments pointing the same way and
neither able to say so.
Files
docs/PERF_EXPRESSION_CODEGEN_NO_WIN.md(new). No production file modified;git diff 6889fba --statshows only that one file.sciMetabolic's MM fix(
8dd441d) is deliberately not on this branch — it is an FP-result changein the same evaluate path and is theirs to ship.
🤖 Generated with Claude Code