You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
HermitCrab engine: share frozen Shapes on internal Word clones (clone 36% to 9% of profile; peak memory 11 GB to 6 GB) #593
Internal Word clones deep-copy the whole Shape (every node, feature structure and annotation), but three of the four clone sites never use the copy. The change adds Word.CloneForEngine, which shares a frozen source shape and marks it shared. EnsureOwnShape copies on demand before in-place mutation, and is called in AnalysisStratumRule and SynthesisStratumRule. ResetShape and Shape.CreateEmptyLike replace clone-then-clear at the four GenerateShape sites. Public Word.Clone() still returns a deep copy. A missed mutation throws because the shared shape is frozen. This is sharing, not a cache. CloneForEngine, EnsureOwnShape, ResetShape and CreateEmptyLike are on the archived branch only.
Measured. Profile on the heaviest Sena word: Word clone 36% to 9% of wall time, Shape.CopyTo 28.6% to 13%, ReplayOnto 21.7% to 6.5%. Peak working set 11 GB to 6 GB. Wall-time attribution is unreliable on that machine. Counts are unchanged by construction.
What a revival must do first. The disposition says to reopen only if memory becomes the constraint, because the aliasing rules are subtle. Re-measure with a gloss, allomorph-index and feature-structure signature, since the old parity signature checked only morph counts.
Archived from docs/hc-optimization-ledger @ 2eba2f5 (docs/optimizations/shape-sharing-on-clone.md), which is being deleted:
Copy-on-write Shape sharing in Word.Clone
Status: retained on perf/hc-optimization-archive, not merged (Word.CloneForEngine, EnsureOwnShape, ResetShape, Shape.CreateEmptyLike). Portable: partly. The observation (three of four clone sites
never use the copied shape) is engine-independent; the mechanism (shared reference to an immutable
structure) is how a Rust port would naturally write it already.
The observation
A CPU profile of the heaviest Sena word (60 s sampled, memoized path) put Word cloning at 36% of wall,
of which Shape.CopyTo was 28%: every clone deep-copied every ShapeNode, every node's feature
structure (SimpleFeatureValue.CloneImpl 22%), every annotation, and built a node-mapping dictionary.
The callers: Word.ReplayOnto 15% (memo replay; never mutates the shape), Word.ExpandAlternatives 9%
(immediately replaces_shape), AnalysisAffixProcessAllomorphRuleSpec.ApplyRhs 5% (GenerateShape
immediately Clear()s the copy), non-head clones 2.5%, LexicalLookup (the RootAllomorph setter
replaces _shape).
The change
Internal engine clone: if the source shape is frozen, share the reference and mark _shapeShared; otherwise
deep-copy it. Public Word.Clone() always returns a deep, independently mutable shape.
EnsureOwnShape(): clone once, on demand, before in-place mutation. Called in AnalysisStratumRule and SynthesisStratumRule before phonological rules are applied to a fresh clone (rewrite specs insert
nodes and hold node references obtained by matching, so the un-share must precede the match).
ResetShape(): replace with a new empty shape of the same configuration. Used by the four GenerateShape/rebuild sites (analysis affix, analysis compounding subrule, synthesis affix, synthesis
compounding) instead of clone-then-clear.
ExpandAlternatives marks the adopted shape as shared; the RootAllomorph setter and constructors mark
their fresh clones as owned.
The safety net is that a shared shape is frozen: any mutation through a missed site throws
(Shape.CheckFrozen, frozen node feature structures; a missing guard on Annotation.Range was added).
During development the first full test run threw at exactly the two stratum sites, which is how they
were found; no other site surfaced across 122 HermitCrab tests, 464 fixture words and 45 Sena words.
Measured
Profile after the change: Word clone 36% → 9% of wall, Shape.CopyTo 28.6% → 13% (the remainder is
the stratum's EnsureOwnShape and part copying inside GenerateShape), ReplayOnto 21.7% → 6.5%. Peak
working set on the heaviest word 11 GB → 6 GB. Wall-time attribution is unreliable on this machine (see
the round document); counts are unchanged by construction.
WordShapeSharingTests covers public-clone independence, copy-on-write, reset paths, and missed-mutation
guards. The final conformance-inclusive run passed 603 tests with one expected host-privilege skip.
Reference numbers from the PanGloss port, in case this is revived. In PanGloss, equal shapes and syntactic feature structures made by separate rule applications in one parse are interned to one allocation (sillsdev/PanGloss#249). That is broader than sharing on clone: at a 200M work cap the search held 379,394 words over only 160 distinct shapes and 42 distinct syntactic feature structures, and the private copies were 690 MB of 1,152 MB. Word mindeshi (Mbugwe), peak resident set, one thread:
work cap
before
shape + feature-structure sharing
50M
199 MB
82 MB
200M
1,152 MB
389 MB
1G
5,074 MB
1,408 MB
Results were identical on four grammars. Cost: Swahili -0.55%, Amharic +0.51% instructions. These are Rust numbers, not C# ones. If the clone-sharing here leaves equal-but-separate shapes from different rule applications, interning is the follow-on idea. Remaining per-word growth is tracked in #639 (finish each stratum as it completes) and #640 (store folded alternatives as differences).
WIN alloc -35..-47% vs master, wall -23..-37% (Amharic -37%, Mbugwe -23%, Aweti -3%), CPU -5..-24%, mbugwe 250-word peak RSS 2.40->1.88 GB | parse output identical | not pushed, no PR (owner)
machine #593: internal Word clones share a frozen Shape (PanGloss #232 idea)
Ready to post as a comment on sillsdev/machine #593. Branch shape-share, commit 8c96a4d7, off origin/master 0cbf85f. Not pushed. Per owner, no PR body written and no further code.
Change (12 files, +153/-17; 76 of those lines are a new test file)
Word.CloneForEngine() (internal): if the source shape is frozen, share it (_shapeShared), else deep-copy. Public Word.Clone() and the protected copy ctor stay deep. Non-head words are engine-cloned too.
EnsureOwnShape(): copy once before in-place edits; used in SynthesisStratumRule before the phonological rules. ResetShape() + Shape.CreateEmptyLike(): replaces clone-then-Clear() in the four rule-spec sites (AnalysisAffixProcessAllomorph, AnalysisCompoundingSubrule, SynthesisAffixProcessAllomorph, SynthesisCompounding).
Engine clones now at: Morpher lexical lookup (2), AnalysisAffixTemplateRule, SynthesisStratumRule (2), SynthesisAffixTemplatesRule (2), AnalysisCompoundingRule (1), Word.ExpandAlternatives. AnalysisStratumRule stays a deep clone (phonological rules mutate and hold node references, so sharing would be undone at once).
Safety net: a shared shape is frozen, so a missed mutation throws rather than corrupting. None surfaced in tests or word lists.
Identity (sorted parses per word: surface, shape string, morph list with morpheme id/gloss/allomorph index, syntactic FS, realizational FS, MPR features, partial flag)
Compared master vs shape-share vs shape-share+#633, byte-for-byte (cmp): all IDENTICAL.
list
words
with parses
Aweti
all 206
157
Amharic
all 673
190
Mbugwe
first 250
239
Sena
154-word sample (every 40th) + first 100
69 + 34
Limits: Mbugwe line 283 (chaanjiyɛ, the known >16 GB word) excluded; master hit the 6 GB heap limit (OOM) at word 304 on a 400-word run, so Mbugwe was cut to 250. Sena is a sample, not 6146 words.
Conformance: fieldworks-parser-conformance run through machine's hermitcrab-conformance-runner branch (merged onto master and onto shape-share, HC_CONFORMANCE_SUITE), with the runner's PR633_DUMP hook logging every word's signature: 1414 word results, sorted, identical on both. The 4 conformance gate tests that fail (CoverageReport_*, CheckedInCoverageTablesAreUpToDate, EveryConformanceFixturePassesSelfCheck, EveryFixtureContributesAtLeastOneCoverageRow) fail the same four on unmodified master+runner (suite version vs the pin), unrelated.
Tests
HermitCrab.Tests 181 -> 186 passed (5 new WordShapeSharingTests: public clone stays deep, engine clone shares frozen/copies unfrozen, shared shape rejects mutation, EnsureOwnShape, ResetShape). SIL.Machine.Tests 910 passed, 3 skipped (preexisting), 0 failed. dotnet csharpier check . clean. Not run red-without-change for the new tests (they call the new methods).
A/B (interleaved, order randomised per round, n=15, new process per sample, 1 warm + 2 timed passes, getrusage CPU and ru_maxrss per child, load avg 10-20 on 20 cores, nice, GC heap hard limit 6 GB)
Arms: B = master; M = master+#633 (alloc-medium e6852c4); S = master+shape-share; SM = both (clean merge). Medians.
workload
arm
alloc MB/pass
wall ms
CPU s
peak RSS MB
GC pause ms
Amharic 40
B
2747
2428
11.33
121
572
M
2251
2081
10.02
118
447
S
1471
1556
8.69
115
344
SM
1239
1344
8.13
114
272
Aweti 15
B
492
739
7.57
248
102
M
439
698
7.31
220
98
S
305
752
7.29
243
85
SM
285
645
6.95
214
84
Mbugwe 12
B
1419
1597
11.55
231
264
M
1216
1440
10.99
229
241
S
794
1221
10.21
223
206
SM
708
1123
9.63
224
188
Paired ratios (median; wins = rounds where the new arm is lower):
Peak memory on a long run (word-list dump, 250 Mbugwe words, one process, /usr/bin/time max RSS): master 2.40 GB, shape-share 1.88 GB (-22%), both 2.60 GB (not lower; single run, noisy, not repeated). Aweti dump (206 words): 516 -> 417 MB (S), 497 MB (SM). Wall for the same dumps: Mbugwe 717 s -> 442 s (-38%), Amharic 37 s -> 24 s, Aweti 29 s -> 24 s. These are single runs under load.
Wall and CPU wins are solid on Amharic (-37% / -24%) and Mbugwe (-23% / -10%), smaller and noisy on Aweti (small workload; S/M wall is slightly worse, 6/15). CPU is less noisy than wall under this load.
Peak RSS of the short benchmarks is dominated by the loaded grammar (110-250 MB), so it barely moves (-1 to -4%). The memory effect the HermitCrab optimization round record: what was tried, and a faster package that is not mergeable #490 note reported (11 -> 6 GB on the heaviest Sena word) is NOT reproduced here: I did not find that word (the heavy Mbugwe word chaanjiyɛ is a different, unbounded-growth case that sharing does not cure). The only memory evidence is the 250-word Mbugwe run (-22%).
Sena heavy-word memory claim (11 -> 6 GB) not re-measured; no perf counters (not installed), so CPU is getrusage time. Identity on Sena and Mbugwe is a subset. Concurrency (MaxDegreeOfParallelism > 1) not exercised; shared frozen shapes are read-only so it should be safe, untested.
Activity
johnml1135 commentedon Nov 7, 2026
Archived from
docs/hc-optimization-ledger@ 2eba2f5 (docs/optimizations/shape-sharing-on-clone.md), which is being deleted:Copy-on-write Shape sharing in
Word.CloneStatus: retained on
perf/hc-optimization-archive, not merged (Word.CloneForEngine,EnsureOwnShape,ResetShape,Shape.CreateEmptyLike). Portable: partly. The observation (three of four clone sitesnever use the copied shape) is engine-independent; the mechanism (shared reference to an immutable
structure) is how a Rust port would naturally write it already.
The observation
A CPU profile of the heaviest Sena word (60 s sampled, memoized path) put
Wordcloning at 36% of wall,of which
Shape.CopyTowas 28%: every clone deep-copied everyShapeNode, every node's featurestructure (
SimpleFeatureValue.CloneImpl22%), every annotation, and built a node-mapping dictionary.The callers:
Word.ReplayOnto15% (memo replay; never mutates the shape),Word.ExpandAlternatives9%(immediately replaces
_shape),AnalysisAffixProcessAllomorphRuleSpec.ApplyRhs5% (GenerateShapeimmediately
Clear()s the copy), non-head clones 2.5%,LexicalLookup(theRootAllomorphsetterreplaces
_shape).The change
_shapeShared; otherwisedeep-copy it. Public
Word.Clone()always returns a deep, independently mutable shape.EnsureOwnShape(): clone once, on demand, before in-place mutation. Called inAnalysisStratumRuleandSynthesisStratumRulebefore phonological rules are applied to a fresh clone (rewrite specs insertnodes and hold node references obtained by matching, so the un-share must precede the match).
ResetShape(): replace with a new empty shape of the same configuration. Used by the fourGenerateShape/rebuild sites (analysis affix, analysis compounding subrule, synthesis affix, synthesiscompounding) instead of clone-then-clear.
ExpandAlternativesmarks the adopted shape as shared; theRootAllomorphsetter and constructors marktheir fresh clones as owned.
The safety net is that a shared shape is frozen: any mutation through a missed site throws
(
Shape.CheckFrozen, frozen node feature structures; a missing guard onAnnotation.Rangewas added).During development the first full test run threw at exactly the two stratum sites, which is how they
were found; no other site surfaced across 122 HermitCrab tests, 464 fixture words and 45 Sena words.
Measured
Profile after the change:
Wordclone 36% → 9% of wall,Shape.CopyTo28.6% → 13% (the remainder isthe stratum's
EnsureOwnShapeand part copying insideGenerateShape),ReplayOnto21.7% → 6.5%. Peakworking set on the heaviest word 11 GB → 6 GB. Wall-time attribution is unreliable on this machine (see
the round document); counts are unchanged by construction.
WordShapeSharingTestscovers public-clone independence, copy-on-write, reset paths, and missed-mutationguards. The final conformance-inclusive run passed 603 tests with one expected host-privilege skip.
johnml1135 commentedon Nov 12, 2026
Reference numbers from the PanGloss port, in case this is revived. In PanGloss, equal shapes and syntactic feature structures made by separate rule applications in one parse are interned to one allocation (sillsdev/PanGloss#249). That is broader than sharing on clone: at a 200M work cap the search held 379,394 words over only 160 distinct shapes and 42 distinct syntactic feature structures, and the private copies were 690 MB of 1,152 MB. Word
mindeshi(Mbugwe), peak resident set, one thread:Results were identical on four grammars. Cost: Swahili -0.55%, Amharic +0.51% instructions. These are Rust numbers, not C# ones. If the clone-sharing here leaves equal-but-separate shapes from different rule applications, interning is the follow-on idea. Remaining per-word growth is tracked in #639 (finish each stratum as it completes) and #640 (store folded alternatives as differences).
johnml1135 commentedon Nov 12, 2026
WIN alloc -35..-47% vs master, wall -23..-37% (Amharic -37%, Mbugwe -23%, Aweti -3%), CPU -5..-24%, mbugwe 250-word peak RSS 2.40->1.88 GB | parse output identical | not pushed, no PR (owner)
machine #593: internal Word clones share a frozen Shape (PanGloss #232 idea)
Ready to post as a comment on sillsdev/machine #593. Branch
shape-share, commit 8c96a4d7, off origin/master 0cbf85f. Not pushed. Per owner, no PR body written and no further code.Change (12 files, +153/-17; 76 of those lines are a new test file)
perf/hc-optimization-archive:CloneForEngine,EnsureOwnShape,ResetShape), reimplemented minimally on master (the archive is on a memoized, older base and bundles HermitCrab engine: share frozen syntactic feature structures on internal Word clones (about 15% of a heavy Sena profile, never isolated) #595 syntactic-FS sharing, which is NOT included here).Word.CloneForEngine()(internal): if the source shape is frozen, share it (_shapeShared), else deep-copy. PublicWord.Clone()and the protected copy ctor stay deep. Non-head words are engine-cloned too.EnsureOwnShape(): copy once before in-place edits; used inSynthesisStratumRulebefore the phonological rules.ResetShape()+Shape.CreateEmptyLike(): replaces clone-then-Clear()in the four rule-spec sites (AnalysisAffixProcessAllomorph, AnalysisCompoundingSubrule, SynthesisAffixProcessAllomorph, SynthesisCompounding).Identity (sorted parses per word: surface, shape string, morph list with morpheme id/gloss/allomorph index, syntactic FS, realizational FS, MPR features, partial flag)
Compared master vs shape-share vs shape-share+#633, byte-for-byte (
cmp): all IDENTICAL.hermitcrab-conformance-runnerbranch (merged onto master and onto shape-share,HC_CONFORMANCE_SUITE), with the runner's PR633_DUMP hook logging every word's signature: 1414 word results, sorted, identical on both. The 4 conformance gate tests that fail (CoverageReport_*, CheckedInCoverageTablesAreUpToDate, EveryConformanceFixturePassesSelfCheck, EveryFixtureContributesAtLeastOneCoverageRow) fail the same four on unmodified master+runner (suite version vs the pin), unrelated.Tests
HermitCrab.Tests 181 -> 186 passed (5 new
WordShapeSharingTests: public clone stays deep, engine clone shares frozen/copies unfrozen, shared shape rejects mutation, EnsureOwnShape, ResetShape). SIL.Machine.Tests 910 passed, 3 skipped (preexisting), 0 failed.dotnet csharpier check .clean. Not run red-without-change for the new tests (they call the new methods).A/B (interleaved, order randomised per round, n=15, new process per sample, 1 warm + 2 timed passes, getrusage CPU and ru_maxrss per child, load avg 10-20 on 20 cores, nice, GC heap hard limit 6 GB)
Arms: B = master; M = master+#633 (alloc-medium e6852c4); S = master+shape-share; SM = both (clean merge). Medians.
Paired ratios (median; wins = rounds where the new arm is lower):
Peak memory on a long run (word-list dump, 250 Mbugwe words, one process, /usr/bin/time max RSS): master 2.40 GB, shape-share 1.88 GB (-22%), both 2.60 GB (not lower; single run, noisy, not repeated). Aweti dump (206 words): 516 -> 417 MB (S), 497 MB (SM). Wall for the same dumps: Mbugwe 717 s -> 442 s (-38%), Amharic 37 s -> 24 s, Aweti 29 s -> 24 s. These are single runs under load.
Reading it
Risks / not done
shape-share-on633,ss-conf-base,ss-conf-shapeunder ~/work/machine.worktrees (local only, delete with the branches conf-base-tmp, conf-shape-tmp). Bench harness and raw data: lanes/machine-shape-share-bench (ab-results.txt, dumps/, ab2.py, sum3.py).