Skip to content

Stack RUSTIFY's array/COW rearchitecture on top of memoization#457

Open
johnml1135 wants to merge 2 commits into
feature/memoizationfrom
feature/memoization-plus-cow
Open

Stack RUSTIFY's array/COW rearchitecture on top of memoization#457
johnml1135 wants to merge 2 commits into
feature/memoizationfrom
feature/memoization-plus-cow

Conversation

@johnml1135

@johnml1135 johnml1135 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #456 (memoization). Answers a question that PR left open: does the missing
copy-on-write Shape/ShapeNode (an array-backed rearchitecture from the previously-reviewed
hc-rustify branch, PR #446) meaningfully improve on memoization alone, or is it a "kind of, but
not really" difference?

This PR's diff is exactly the general allocation/data-structure rearchitecture from PR #446
(flat/COW Shape, int-offset FST traversal, cheap hash overrides, etc.) cherry-picked onto
feature/memoization and merge-resolved — not the FST inverse-chain analyzer or grammar
linter from the separate, still-experimental fst-advisor exploration branch. That's a
different optimization strategy (compile-to-FST vs. memoize-the-search) with its own,
independently-confirmed correctness gap on pathological words; out of scope here.

Cost of this PR: a much larger diff than #456 (112 files vs. 11) touching the engine's core
Shape/ShapeNode data representation, not just the analysis cascade. It was independently
reviewed once already as PR #446; this PR re-verifies it against the current codebase plus
memoization's new code paths (merge notes below), but reviewers should weigh that scope against
the numbers below, not just the numbers alone. #456's off-by-default safety property is
unchanged here — the array/COW rearchitecture activates for every Morpher, memoized or not,
same as it did in #446, and the memo itself is still only reachable via
maxDegreeOfParallelism: 1.

Result: a real improvement, apples to apples

Same methodology as memoization.md (#456): canonical analysis-set signature comparison (never
byte-identical), same two heavy words used to verify memoization alone, same local uncommitted
Sena grammar.

Word memo-on memo-off Ratio Divergences vs. memoization alone (#456)
H1 (known heavy, positive analysis) 9.17s 90.0s 9.81x 0 was 22.1s/136.1s/6.2x — faster on both sides, better ratio
H2 (second heavy word) 44.8s 263.8s 5.89x 0 (both empty) was >400s both sides — never completed at all before

H2 is the headline: the word memoization alone could not get through even a 400s budget now
completes cleanly in well under a minute. One honest caveat on H2, spelled out in
rustify-cow.md: it resolves to zero valid analyses on both sides in this grammar, so the
soundness check there is "both sides agree on empty," not a positive multi-analysis match like
H1 — still non-vacuous (434,628 nogood hits recorded), just a weaker confirmation.

Cross-language aggregate and multithreading (4 threads)

The H1/H2 numbers above are a deep-dive on two specific pathological words. Separately, to get an
apples-to-apples read across grammars (not just the worst known words), the corpus-verification
harness was re-run with a 200s per-word timeout on larger slices — 250 words each for Sena and
Amharic, all 121 words that exist in the Indonesian list — across four states: baseline
(today's shipped parallel default), memo (#456 alone), cow-only (this PR, no memo), and
memo+cow (this PR stacked on #456). Zero analysis-set divergences across all 12 runs.

Language State Timed out (>200s) Mean (ms) p50 (ms) p95 (ms)
Sena baseline 2/250 3362.4 112.9 16444.2
Sena memo 0/250 1856.0 106.1 6082.4
Sena memo+cow 0/250 932.5 61.1 3993.1
Amharic baseline 6/250 7261.9 297.5 35374.3
Amharic memo 6/250 6460.9 299.5 31561.4
Amharic memo+cow 1/250 2943.2 62.8 14714.6
Indonesian baseline 0/121 17.9 5.8 52.5
Indonesian memo 0/121 20.3 6.1 62.7
Indonesian memo+cow 0/121 15.1 4.2 47.7

Amharic is the important row: memoization alone barely moves the timeout count (6 → 6) or
the mean (~11% better). This PR's rearchitecture is what actually fixes Amharic's pathological
words (timeouts 6 → 1, mean more than halved) — full breakdown, including the cow-only isolation
row, in rustify-cow.md.

Words/second with 4x multithreading (new: MemoFourThreadThroughput_WordsPerSecond, same
word slices, 4 worker threads sharing one sequential+memo Morpher, 200s per-word watchdog;
1-thread Sena/Amharic figures are derived (1000/meanMs) from the table above, not separately
measured — the Indonesian row is separately measured and the derived estimate there overstated
the real number by ~21%, so treat the derived figures as approximate):

Language Threads memo (words/sec) memo+cow (words/sec) memo+cow vs memo
Sena 1 (derived) 0.54 1.07
Sena 4 1.11 2.40 2.16x
Amharic 1 (derived) 0.15 0.34
Amharic 4 0.11 (18 timeouts) 0.40 (4 timeouts) 3.64x (confounded by the timeout-count gap, not a clean multiplier)
Indonesian 1 (measured) 40.75 56.55
Indonesian 4 46.50 100.35 2.16x

This confirms "COW enables better multithreading" directionally, but the honest shape of it
differs from a flat "extra 2x": on Sena and Indonesian, this PR's 1→4 thread scaling really is
better than memo-only's (2.24x vs 2.06x; 1.77x vs 1.14x, the latter pair both real measurements).
On Amharic, the finding isn't extra speed so much as robustness under contention — at 4
threads, memo-only starts missing the 200s watchdog on 3x more words (18 vs 6 single-threaded)
than it did running alone, while memo+cow only loses the watchdog on 1 more (4 vs 1). COW is the
difference between concurrent Amharic parsing holding up and falling over, more than it is a
uniform 2x bonus.

Merge notes and an anomaly, not hidden

Full writeup — including the merge-resolution reasoning for the 4 files both branches touched —
is in rustify-cow.md. One thing worth flagging directly rather than leaving to the doc: the
first H1 measurement, at a 240s budget, timed out, which is odd since 9.17s + 90.0s is nowhere
near 240s. A re-run at 600s completed cleanly with the numbers above. The doc treats the
transient-load explanation (this session ran many consecutive heavy benchmarks right before) as
plausible, not proven — the memo-hit counters from the failed run matched the completed
run's final tally, which is suggestive that the computation had actually finished by read-time,
but not conclusive. Flagging this as an open, not fully closed, question rather than asserting
it's settled.

Test plan

  • dotnet build Machine.sln
  • dotnet csharpier check .
  • HermitCrab test suite: 80/80 (78 from Memoize HermitCrab's sequential analysis cascade #456 + 2 new from this branch)
  • Full Machine.sln test suite green
  • Corpus verification harness run manually against local Sena/Amharic/Indonesian grammars — 0 divergences across the single-word deep-dive, the 250/250/121-word cross-language aggregate, and the 4-thread throughput matrix (12 + 6 runs, all states)

This change is Reviewable

@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.99413% with 218 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.82%. Comparing base (8f5438a) to head (ede36d0).

Files with missing lines Patch % Lines
src/SIL.Machine/Annotations/Shape.cs 78.59% 52 Missing and 12 partials ⚠️
src/SIL.Machine/FiniteState/VisitedStates.cs 19.35% 23 Missing and 2 partials ⚠️
...Machine/DataStructures/DataStructuresExtensions.cs 57.50% 16 Missing and 1 partial ⚠️
src/SIL.Machine/FeatureModel/FeatureStruct.cs 86.71% 16 Missing and 1 partial ⚠️
src/SIL.Machine/Annotations/ShapeNode.cs 62.50% 11 Missing and 4 partials ⚠️
.../FiniteState/NondeterministicFstTraversalMethod.cs 67.39% 15 Missing ⚠️
...hine.Morphology.HermitCrab/HermitCrabExtensions.cs 72.22% 8 Missing and 2 partials ⚠️
src/SIL.Machine/FiniteState/Fst.cs 83.92% 8 Missing and 1 partial ⚠️
src/SIL.Machine/FiniteState/Input.cs 73.07% 6 Missing and 1 partial ⚠️
.../FiniteState/NondeterministicFsaTraversalMethod.cs 82.14% 4 Missing and 1 partial ⚠️
... and 15 more
Additional details and impacted files
@@                   Coverage Diff                   @@
##           feature/memoization     #457      +/-   ##
=======================================================
+ Coverage                73.47%   73.82%   +0.35%     
=======================================================
  Files                      446      447       +1     
  Lines                    37398    38114     +716     
  Branches                  5140     5282     +142     
=======================================================
+ Hits                     27477    28138     +661     
- Misses                    8791     8833      +42     
- Partials                  1130     1143      +13     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Cherry-picks the previously-reviewed allocation/data-structure rearchitecture
(PR #446: flat, copy-on-write Shape/ShapeNode backed by parallel int arrays
instead of a linked list; int-offset FST traversal throughout) onto
feature/memoization and resolves the resulting merge conflicts across the
four files both branches touched.

Answers the open question from memoization.md: on the one heavy word already
verified sound under memoization alone, this measures 9.81x faster (vs 6.2x)
with both sides faster individually. On the second heavy word -- which never
completed within a 400s budget under memoization alone -- this completes in
44.8s at a 5.89x ratio, with 0 divergences on both. Typical (non-template)
grammars are unaffected either way. Full methodology, merge-resolution
reasoning, and honest caveats (including a first anomalous timing run traced
to transient machine load, not a regression) are in rustify-cow.md.
@johnml1135
johnml1135 force-pushed the feature/memoization-plus-cow branch from c0b0610 to 88ed8f3 Compare July 17, 2026 11:24
Per-side timeout attribution in the corpus-verification harness, plus a new
MemoFourThreadThroughput_WordsPerSecond method measuring cross-word batch
throughput at a fixed thread count. Adds the resulting 250/250/121-word
Sena/Amharic/Indonesian data (baseline/memo/cow-only/memo+cow, 200s watchdog)
and the 1-vs-4-thread words/sec comparison to rustify-cow.md.
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.

2 participants