Skip to content

Hash an ingredient key once per index update - #241

Open
rubensworks wants to merge 2 commits into
master-1.21-ltsfrom
perf/ingredient-map-compute-1.21
Open

Hash an ingredient key once per index update#241
rubensworks wants to merge 2 commits into
master-1.21-ltsfrom
perf/ingredient-map-compute-1.21

Conversation

@rubensworks

Copy link
Copy Markdown
Member

The 1.21 version of #239.

An ingredient map hashes its key on every operation, and for ItemStack that hash walks a data component map. The update paths all read then write, and IngredientMapWrappedAdapter builds its own wrapper inside each of get, put and remove, so one logical update hashed the same key twice. Storage networks then run every change twice over, once for its own channel and once for the wildcard channel, so it multiplies.

IIngredientMapMutable gains a compute method with Map.compute semantics. The default implementation is the old read-then-write, so every existing implementation stays correct without changing. IngredientMapWrappedAdapter builds one wrapper and hands it to the backing map's own compute. IngredientMapSingleClassified delegates to the classifier's sub-map while keeping its size counter and its empty-classifier cleanup.

IngredientCollectionPrototypeMap.add and remove use it. Component types whose hash is cheap see no difference either way.

The companion change on the IntegratedDynamics side is CyclopsMC/IntegratedDynamics#PENDING_ID_COMPUTE, which converts IngredientPositionsIndex.addPosition and removePosition.

Numbers

These come from 26.1, not from 1.21. All four touched files are byte identical between master-1.21-lts and master-26-lts, so the change is literally the same code, but I did not re-run the benchmarks here. Say the word and I will; each full run on this branch takes about 33 minutes, so a paired set is a couple of hours.

IntegratedDynamics index benchmarks on 26.1, ms per operation, medians of six runs each, alternating between the two configurations within one session so session drift falls on both equally. Both sides carry the fixes from #238.

benchmark without compute with compute factor
index_modification_single_item 0.001162 0.000688 41% faster
index_modification_few_items 0.001084 0.000726 33% faster
index_modification_heavy_components 0.002510 0.002077 17% faster
index_modification_mixed 0.000713 0.000667 6% faster
index_modification_plain 0.000530 0.000507 4% faster
index_modification 0.001246 0.001381 11% slower

Lookup benchmarks all land within 10% either way with no consistent direction, which is expected: they do not read-then-write, so there is nothing for this to collapse.

The win is concentrated where hashing genuinely dominates a modification: many component variants of one item, or a large component payload. It is much weaker on the realistic shapes, because #238 already skips the component hash entirely for stacks carrying no patch, so there is little hashing left to halve. The spread-shape row reads 11% slower, but that benchmark runs first in the sequence and absorbs JIT warm-up, and its two distributions overlap almost completely; I would not read a regression into it and cannot rule one out either.

I predicted this would be worth about a third of the remaining modification cost. That held for the hash-dominated shapes and overestimated the realistic ones. Reporting the miss rather than the prediction.

One caveat specific to this branch: on 1.21 the hash fix makes modifications faster rather than slower, because the baseline collides much harder here. That means the cost this change exists to reduce is a smaller share of the total on 1.21 than on 26.1, so the benefit here is probably smaller than the table above suggests.

Recommendation

Worth taking, but not urgently. It is a strict reduction in work, the API mirrors Map.compute so it is not exotic, and the default keeps every other implementation correct untouched. But the realistic-mix gain is inside the measurement spread, so if you would rather not widen IIngredientMapMutable for that, closing this is a defensible call.

Tests

TestIngredientMapCompute covers insert, update, remove, a null remapping on an absent key, key pass-through, and size tracking across classifiers, over IngredientHashMap, IngredientTreeMap and IngredientMapSingleClassified. It also drives a sequence of computes against a second map doing the get-then-put by hand and asserts the two stay identical after every step. It is written against this branch's JUnit 4 setup, in a separate commit so the port is visible on its own.

./gradlew build passes on all loaders.

Relationship to the 26.1 branch

#239 is the same change against master-26-lts and stays open. Merge whichever suits your upmerge direction.

Notes for review

  • IngredientMapMultiClassified keeps the default implementation. It is correct, just not collapsed; the positions index never uses it.
  • This is independent of the hash fix and merges cleanly with it.
  • No changelog entry, as requested.

Generated by Claude Code

An ingredient map hashes its key on every operation, and for ItemStack that
hash walks a data component map. The update paths all read then write, and each
call builds its own wrapper, so the same key was hashed twice for one logical
update. Storage networks then run each change twice over, once for its own
channel and once for the wildcard channel, so this multiplies.

IIngredientMapMutable gains a compute method with Map.compute semantics. The
default is the old read-then-write, so nothing has to change to stay correct.
The wrapped adapter builds one wrapper and hands it to the backing map's own
compute, and the classified map delegates to the classifier's sub-map while
keeping its size and its empty-classifier cleanup.

IngredientCollectionPrototypeMap add and remove use it. Callers that hold a
cheap-to-hash component type see no difference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mxjin31W1Lmq5XK1CCe84v
(cherry picked from commit 1ae5111)
This branch uses JUnit 4, so the parameterized test follows the Parameterized
runner pattern the other collection tests here use. The assertions are
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mxjin31W1Lmq5XK1CCe84v

Copy link
Copy Markdown
Member Author

Correcting a placeholder in the description: the companion IntegratedDynamics change is CyclopsMC/IntegratedDynamics#1728.


Generated by Claude Code

@sonarqubecloud

sonarqubecloud Bot commented Sep 7, 2026

Copy link
Copy Markdown

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 26.705% (+0.08%) from 26.63% — perf/ingredient-map-compute-1.21 into master-1.21-lts

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.

3 participants