HermitCrab allocation: FeatureStruct clone dominates; jemalloc not applicable

オープン
#632 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@johnml1135 がすでに取り組んでいます。

2026年11月11日 から。

  • #633 @johnml1135 による — オープン

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
38/100
issue の種類
機能追加
明瞭さ
おおむね明確
活発さ
活発
技術スタック
csharp
領域
performance

調査の方向性

The profiling points to FeatureStruct.Clone and VariableBindings cloning, but the issue does not name source files or tests. Start by locating FeatureStruct.Clone and the copy constructor, then trace callers that clone VariableBindings in Unify, Subsumes, and IsUnifiable. Compare structural sharing or copy-on-write and avoiding the copies map for structs without reentrancies; done means reduced cloning with measurements on a quiet machine using the same harness and workloads.

索引モデルが issue の本文から書いたものです。

説明

HermitCrab allocation: FeatureStruct cloning is about 60% of parse bytes; allocator and GC-mode levers are dead ends
Claude-drafted.

About 60% of HermitCrab parse allocation bytes come from FeatureStruct clone machinery, and no allocator or GC-mode change removes that cost. The one host-side lever, DOTNET_GCgen0size=10000000 (hex, 256 MB), cuts gen0 collections about 10x for a noisy wall-time gain of roughly 6 to 14%. The next step is to reduce the cloning itself, not to tune the allocator.

What we know (numbers and method in the details):

  • The clone machinery (the copies Dictionary and its buckets, _definite and VariableBindings clones, SymbolicFeatureValue) is the bulk of allocated bytes on Amharic. GC pause is 450-670 ms of a 2.4-3.1 s pass, 15-25% of wall time.
  • jemalloc via LD_PRELOAD and jemalloc.NET do not apply. Under 1% of allocation events are native, and the jemalloc timing matched a no-op GC control.
  • Server GC, non-concurrent GC, TieredPGO=0 and TieredCompilation=0 gave no win. The last two were 32-35% slower on Amharic.
  • The pooled copies scratch map cuts allocated bytes 10-17%, but wall time is mixed across grammars, so it is not proposed as a PR.

Next: reduce FeatureStruct cloning (structural sharing or copy-on-write, and no copies map for structs without reentrancies) and re-measure on a quiet machine with the same harness. Related open issues: #586 and #587.

Where the bytes go (GCAllocationTick sampling, Amharic 40 words, about 55k ticks)
  • 13.1% Entry[FeatureValue,FeatureValue][] and 12.1% Int32[] (Dictionary buckets): the per-clone copies map in the FeatureStruct copy constructor, plus the _definite maps.
  • 12.4% Entry[string,FeatureValue][]: VariableBindings clones in Unify, Subsumes and IsUnifiable. VariableBindings clones on every call.
  • 12.3% Entry[Feature,FeatureValue][]: _definite of each cloned FeatureStruct.
  • 11.2% SymbolicFeatureValue, 7.0% UlongSymbolicFeatureValueFlags, 3.4% Annotation, 2.8% FSA Register arrays.
  • Net: about 60% of bytes are FeatureStruct clone machinery. FeatureStruct.Clone allocates a Dictionary, buckets and entries per clone even when the struct has no reentrancies.
Method and caveats
  • Harness: a benchmark program in the machine worktree (branch alloc-bench, built against origin/master 0cbf85f1, not pushed). It loads PanGloss samples/data for amharic, aweti and mbugwe (*-hc.xml), builds Morpher(..., maxDegreeOfParallelism 1), and parses the first N words of the matching word list.
  • Per process: 1 warm pass and 2 timed passes; a new process per sample; configs interleaved per round; net10.0 Release.
  • Each cell is n=5 to 15. "Median" is the mean of the two timed passes per process, then the median across processes.
  • Machine load average ran 7.4 to 24.5 on 20 cores. Absolute times are not comparable with a quiet machine, and wall-time spread is 20-40%.
  • Single thread only. The default multi-word parallel case under Server GC was not measured, and the Morpher default parallelism and Server GC interplay was not studied.
  • The Sena word list in PanGloss samples/data parsed zero words in the harness. Not investigated.
Run 2: Amharic 40 words, 8 rounds, interleaved (most controlled)
config median ms min-max vs base GC pause ms gen0/1/2 GCs
DOTNET_GCgen0size=0x10000000 (256 MB) 2668 2361-3152 -13.5% 450 21/2/0
gen0size 256 MB + gcConcurrent=0 2708 2373-3478 -12.2% 569 21/0/0
gen0size 1 GB 2786 2358-3260 -9.7% 470 21/2/0
GCgen0MaxBudget=1 GB (no-op control, no GC change) 2862 2256-3741 -7.2% 650 229/90/9
LD_PRELOAD jemalloc 2864 2125-3354 -7.2% 636 228/80/8
base 3086 2497-4010 0 670 229/89/9
gcConcurrent=0 3268 2326-4469 +5.9% 582 84/19/3

The no-op control (GCgen0MaxBudget, identical GC counts) reads -7.2%, the same as jemalloc, so the jemalloc result is noise. Allocated MB is identical in all configs (2745-2747).

Run 1: Amharic 60 words and Aweti 15 words, 5 rounds (medians vs base)
config Amharic Aweti
jemalloc -6.9% -12.9%
gen0size 256 MB -6.2% -14.1%
gen0size 64 MB +2.7% +26.1%
gcServer=1 +4.9% -4.9%
gcConcurrent=0 +9.2% +8.4%
TieredPGO=0 +32.0% -3.9%
TieredCompilation=0 +35.2% +11.6%

Base spreads: 2790-3838 ms (Amharic) and 706-1052 ms (Aweti). Effects under about 10% are not distinguishable from noise.

Why GC config is not a PR
  • GCgen0size is the only GC knob with a consistent direction: -6 to -14% in two batches, GC pause 670 to 450 ms, gen0 GCs 229 to 21.
  • It has no runtimeconfig name (environment variable only), so a library cannot set it, and a host's GC mode is the host's decision.
  • The gain is within about 2x the noise floor.
  • Proposed docs-only note for hosts: "DOTNET_GCgen0size=10000000 (hex, 256 MB) may cut HermitCrab gen0 collections 10x; measure first."
  • GCgen0MaxBudget raised to 1 GB did nothing (equal to base within noise).
Pooled `copies` scratch map in FeatureStruct.Clone (code lever, not proposed as a PR)

The patch takes a thread-static dictionary out of its slot, clears it after each Clone, and is not a result cache. Bytes allocated per pass are deterministic: Amharic 2747 to 2292 MB (-17%), Aweti 493 to 442 MB (-10%).

Interleaved A/B, same build otherwise:

workload n median ms base to pool paired ratio (pool/base) pool faster
Amharic 40 (run 3) 10 2566 to 2362 (-8.0%) 0.883 8/10
Aweti 15 (run 3) 10 793 to 760 (-4.2%) 1.028 3/10
Amharic 40 (run 4) 15 2671 to 2457 (-8.0%) 0.954 11/15
Mbugwe 12 (run 4) 15 1791 to 1741 (-2.8%) 1.018 6/15

Byte cuts of 10-17% do not become a repeatable wall-time win. The Amharic gain is the only one at the 5% level, and it is smaller in paired ratio than in medians. Aweti and Mbugwe are flat to slightly slower on paired ratio. It is a candidate to revisit on a quiet machine with CPU counters.

jemalloc and jemalloc.NET
  • jemalloc LD_PRELOAD: a full Aweti parse made 28,562 native mallocs (about 10 MB live) against 493 MB of managed allocation in one pass. The GC reserves through mmap, bypassing malloc. Native malloc is well under 1% of allocation events.
  • jemalloc.NET (allisterb): last commit and release v0.2.4 on 2018-01-11, so unmaintained since 2018. It targets netcoreapp2.0 (C# 7.1), has no NuGet package, and ships Windows-oriented native binaries with no Linux binaries. It offers native buffers of unmanaged primitives only. HermitCrab's hot data are reference-type object graphs, so there is no site where a primitive buffer would replace the dominant allocations. Machine ships netstandard2.0 libraries, so the dependency is not acceptable.
主要言語
C#
スター
29
フォーク
16
平均マージ
4日 11時間
マージ済み PR(30日)
14

環境構築

  • Dockerfile・Docker Compose ファイルなし
  • プルリクエストのテンプレートあり
  • コントリビューションガイドなし

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

sillsdev/machine のほかの issue

sillsdev/machine の issue をすべて見る

似ている issue

C# の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。