Add non-boxing forEach traversal to the Swiss map matrix#13
Open
alexander-yevsyukov wants to merge 4 commits into
Open
Add non-boxing forEach traversal to the Swiss map matrix#13alexander-yevsyukov wants to merge 4 commits into
alexander-yevsyukov wants to merge 4 commits into
Conversation
The primitive-value maps (LongLongMap, IntIntMap, and the four other cells) exposed no iteration API, so the comparative `iterate` benchmark op could not include them, though every competitor primitive-map library offers a non-boxing traversal. Add a `forEach` member plus a primitive-specialized callback interface, generated from the codegen templates: - Primitive maps: e.g. `LongLongConsumer.accept(Long, Long)` — neither key nor value boxed. A plain `(K, V) -> Unit` erases to `Function2` and boxes both; a public `inline fun` may not touch the internal `Swar` or private storage, so a functional interface is the only non-boxing path. - Boxed maps: e.g. `LongObjectConsumer<V>.accept(Long, V)` — the primitive key is unboxed (the value is already an object). `forEach` reuses the table's existing full-lane control-word walk, so it stays allocation-free and off the hot path; the primitive maps keep no modCount, so the contract is "do not structurally modify during the walk" rather than fail-fast. Iteration order is unspecified. Wire the comparative `iterate` op into the benchmark harness across the boxing gradient — LongLongMap (boxes neither) and SwissLongMap (value only) against the HashMap baseline (both) — plus a self-contained IntIntMap primitive-vs-boxed pair. Regenerate the checked-in sources: verifyGeneratedSwissMaps is clean and :elastic:check passes (all KMP targets, detekt, Kover). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a non-boxing forEach traversal API across the Swiss map “matrix” (primitive-value and boxed-value variants), generates the corresponding primitive-specialized consumer SAM interfaces, and wires a new iterate operation into the benchmark harness. It also adds deterministic + property tests to ensure forEach visits each entry exactly once, and bumps the published snapshot version.
Changes:
- Add
forEachmember traversal to primitive maps (*Map) and boxed Swiss maps (Swiss*Map), plus generated*Consumercallback interfaces to avoid boxing. - Extend test suites (deterministic + property-based) to validate
forEachcorrectness across the matrix. - Add
iteratebenchmarks for Swiss, primitive, and stdlib baselines; bump snapshot version and regenerate dependency reports/templates.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| version.gradle.kts | Bumps publish version to 1.0.0-SNAPSHOT-011. |
| elastic/src/commonMain/kotlin/io/spine/elastic/SwissLongMap.kt | Adds non-boxing-key forEach and LongObjectConsumer. |
| elastic/src/commonMain/kotlin/io/spine/elastic/SwissIntMap.kt | Adds non-boxing-key forEach and IntObjectConsumer. |
| elastic/src/commonMain/kotlin/io/spine/elastic/LongLongMap.kt | Adds fully non-boxing forEach and LongLongConsumer. |
| elastic/src/commonMain/kotlin/io/spine/elastic/LongIntMap.kt | Adds fully non-boxing forEach and LongIntConsumer. |
| elastic/src/commonMain/kotlin/io/spine/elastic/LongDoubleMap.kt | Adds fully non-boxing forEach and LongDoubleConsumer. |
| elastic/src/commonMain/kotlin/io/spine/elastic/IntLongMap.kt | Adds fully non-boxing forEach and IntLongConsumer. |
| elastic/src/commonMain/kotlin/io/spine/elastic/IntIntMap.kt | Adds fully non-boxing forEach and IntIntConsumer. |
| elastic/src/commonMain/kotlin/io/spine/elastic/IntDoubleMap.kt | Adds fully non-boxing forEach and IntDoubleConsumer. |
| elastic/src/commonTest/kotlin/io/spine/elastic/SwissLongMapViewSpec.kt | Adds deterministic forEach “visits every entry” test for boxed Swiss map view. |
| elastic/src/commonTest/kotlin/io/spine/elastic/SwissIntMapViewSpec.kt | Adds deterministic forEach “visits every entry” test for boxed Swiss map view. |
| elastic/src/commonTest/kotlin/io/spine/elastic/LongLongMapSpec.kt | Adds deterministic forEach exact-once test for LongLongMap. |
| elastic/src/commonTest/kotlin/io/spine/elastic/LongLongMapPropertiesSpec.kt | Adds property test comparing forEach output to a HashMap model. |
| elastic/src/commonTest/kotlin/io/spine/elastic/LongIntMapSpec.kt | Adds deterministic forEach exact-once test for LongIntMap. |
| elastic/src/commonTest/kotlin/io/spine/elastic/LongIntMapPropertiesSpec.kt | Adds forEach vs model property test for LongIntMap. |
| elastic/src/commonTest/kotlin/io/spine/elastic/LongDoubleMapSpec.kt | Adds deterministic forEach exact-once test for LongDoubleMap. |
| elastic/src/commonTest/kotlin/io/spine/elastic/LongDoubleMapPropertiesSpec.kt | Adds forEach vs model property test for LongDoubleMap. |
| elastic/src/commonTest/kotlin/io/spine/elastic/IntLongMapSpec.kt | Adds deterministic forEach exact-once test for IntLongMap. |
| elastic/src/commonTest/kotlin/io/spine/elastic/IntLongMapPropertiesSpec.kt | Adds forEach vs model property test for IntLongMap. |
| elastic/src/commonTest/kotlin/io/spine/elastic/IntIntMapSpec.kt | Adds deterministic forEach exact-once test for IntIntMap. |
| elastic/src/commonTest/kotlin/io/spine/elastic/IntIntMapPropertiesSpec.kt | Adds forEach vs model property test for IntIntMap. |
| elastic/src/commonTest/kotlin/io/spine/elastic/IntDoubleMapSpec.kt | Adds deterministic forEach exact-once test for IntDoubleMap. |
| elastic/src/commonTest/kotlin/io/spine/elastic/IntDoubleMapPropertiesSpec.kt | Adds forEach vs model property test for IntDoubleMap. |
| codegen/src/main/resources/io/spine/elastic/codegen/template/SwissMap.kt.tpl | Generates forEach + %K%ObjectConsumer into Swiss boxed maps. |
| codegen/src/main/resources/io/spine/elastic/codegen/template/PrimitiveMap.kt.tpl | Generates forEach + %K%%V%Consumer into primitive maps. |
| codegen/src/main/resources/io/spine/elastic/codegen/template/SwissMapViewSpec.kt.tpl | Generates deterministic boxed-Swiss forEach test. |
| codegen/src/main/resources/io/spine/elastic/codegen/template/PrimitiveMapSpec.kt.tpl | Generates deterministic primitive forEach exact-once test. |
| codegen/src/main/resources/io/spine/elastic/codegen/template/PrimitiveMapPropertiesSpec.kt.tpl | Generates primitive forEach vs model property test. |
| benchmarks/src/commonMain/kotlin/io/spine/elastic/benchmark/SwissLongMapBenchmark.kt | Adds iterate benchmark using SwissLongMap.forEach. |
| benchmarks/src/commonMain/kotlin/io/spine/elastic/benchmark/LongLongMapBenchmark.kt | Adds iterate benchmark using LongLongMap.forEach. |
| benchmarks/src/commonMain/kotlin/io/spine/elastic/benchmark/StdlibHashMapBenchmark.kt | Adds iterate benchmark for boxed baseline (needs a small fix). |
| benchmarks/src/commonMain/kotlin/io/spine/elastic/benchmark/IntIntMapBenchmark.kt | Adds iterate and iterateBoxed benchmarks (boxed variant needs a small fix). |
| docs/dependencies/pom.xml | Updates docs dependency version to -011. |
| docs/dependencies/dependencies.md | Regenerates dependency report for -011. |
| .agents/tasks/primitive-map-foreach.md | Adds agent task note for the work (config-managed docs). |
| .agents/memory/MEMORY.md | Updates agent memory index (config-managed docs). |
| .agents/memory/codegen-template-article-check.md | Adds agent memory entry about template article guard (config-managed docs). |
Replace `map.forEach { (k, v) -> }` with `for ((k, v) in map)` in the boxed
`iterate` baselines (StdlibHashMapBenchmark, IntIntMapBenchmark). Behavior is
identical — both walk the entry set and box each key/value — but the loop reads
unambiguously as entry iteration, distinct from the primitive maps' non-boxing
two-arg `forEach { k, v -> }`. Addresses PR review feedback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #13 +/- ##
============================================
+ Coverage 96.98% 97.10% +0.11%
- Complexity 431 455 +24
============================================
Files 22 22
Lines 1857 1933 +76
Branches 285 301 +16
============================================
+ Hits 1801 1877 +76
Misses 26 26
Partials 30 30 🚀 New features to boost your workflow:
|
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.
What & why
The primitive-value maps (
LongLongMap,IntIntMap, and the four other cells)exposed no iteration API — no
forEach, no cursor, not even the boxedasMutableMap()view the boxed maps have. Every competitor primitive-map libraryoffers a non-boxing traversal (fastutil
forEach, HPPC cursors, EclipseforEachKeyValue, AgronalongForEach), and their absence meant the comparativeiteratebenchmark op could not include our maps.This adds a non-boxing
forEachplus a primitive-specialized callback interface,generated from the codegen templates like the rest of the matrix, and wires the
iterateop into the benchmark harness.API
forEach(action: <K><V>Consumer), e.g.LongLongConsumer.accept(Long, Long): neither key nor value boxed.forEach(action: <K>ObjectConsumer<V>), e.g.LongObjectConsumer<V>.accept(Long, V): primitive key unboxed (value is alreadyan object).
Each
Consumeris a top-levelpublic fun interfaceco-located in its map's file,mirroring the existing
LongHasher/IntHasherhouse style. A lambdamap.forEach { key, value -> … }SAM-converts to it.Why a functional interface and not
(K, V) -> Unit: a Kotlin function typeerases to
Function2, whoseinvokeboxes each primitive — exactly the cost thislibrary exists to eliminate. A public
inline funwas also rejected: it may nottouch the
internalSwaror the private storage, so it can't be a publishedentry point.
forEachis therefore a member (it needs that private/internalstate) and reuses the table's existing full-lane control-word walk, so it stays
allocation-free and off the hot path.
Iteration order is unspecified; the contract is "do not structurally modify during
the walk" (matching
java.util.Map.forEach). The primitive maps keep nomodCount, so there is deliberately no fail-fast guard — adding one would tax theallocation-free hot path.
Benchmark: the
iterateopWired across the boxing gradient so one column ranks all three storage strategies
on the same traversal:
LongLongMapBenchmark.iterateSwissLongMapBenchmark.iterateStdlibHashMapBenchmark.iterateIntIntMapBenchmark.iterate/iterateBoxedEach sums
key xor valueinto a primitive sink consumed once through theBlackhole; the boxed arms iterate via entry destructuring ({ (k, v) -> … }) sothe call resolves on Native, not just the JVM
BiConsumeroverload. Running thematrix on pinned hardware and publishing numbers stays Phase 6.
Testing
forEachexact-once test and a randomizedforEach-vs-HashMapproperty, generated across every cell and mirrored by hand into the frozen
LongLongMaporacle specs../gradlew clean build dokkaGenerategreen: all KMP targets, detekt, Kover,verifyGeneratedSwissMaps(zero drift), and Dokka (new KDoc links resolve).Reviews
Ran the repo pre-PR gate:
spine-code-reviewandkotlin-engineerbothAPPROVE with zero Must-fix / Should-fix. Version bumped
1.0.0-SNAPSHOT-010→-011, dependency reports regenerated.🤖 Generated with Claude Code