Replies: 4 comments
Tier C landed — measured outcomePR #118 (Object[] structural intermediate, replacing the
Allocation per op (the structural change, immune to bench noise):
What the gap looks like nowFlat bean → record: 143 ns vs MapStruct 3.5 ns ≈ 41× gap (down from ~78×). Allocation pressure now in a regime where the JIT can scalar-replace the surviving Tier D research — three agents reported inTop-5 consolidated levers, ranked by estimated ns reclaim:
Cumulative headroom: ~80-135 ns → flat bean → record from 143 → ~10-60 ns (~3-17× MapStruct). Big-swing alternative: per-pair specialised flat NextSequence the five levers as small surgical PRs. Re-measure after each so we don't carry compounded noise. Defer the MH-specialised mapper until the surgical wins are quantified. |
Tier D — detailed research findings (3 agents, post-Tier-C)Now that Tier C is measured (52-58% time + 70-86% allocation), we dispatched three read-only research agents in parallel to scope what's next on the runtime path. Their findings, consolidated: Agent 1 — Post-Tier-C ns budget for flat bean → record (~178 ns target)
Sum: ~175 ns (±15 ns band), matches measured 178.4 ns. Agent 2 — Per-pair specialised
|
| # | Lever | Est. ns saved | Source |
|---|---|---|---|
| 1 | Holder emits positional constructArr(Object[]) — kill indexOf O(N²) scan |
30-60 | Agent 3 Q2 |
| 2 | Records-fast-path for holder-annotated records | 15-30 | Agent 3 Q1 |
| 3 | FORWARD_SEEN ThreadLocal hoist for acyclic type pairs |
~15 | Agent 1 entry 8 |
| 4 | Iso.then 3-hop chain fusion into single specialised Iso |
10-15 | Agent 1 top-1 |
| 5 | Capture Function<Object,Object> directly in resolveReadersByPosition |
10-15 | Agent 1 top-2 |
Five surgical PRs, no new abstractions, all lattice-honest. Cumulative target: flat bean → record from 143 → ~10-60 ns, i.e. ~3-17× MapStruct (down from 41×).
After those land, the big-swing MH-composed specialised flat Mapper (Agent 2) targets 15-25 ns for eligible pairs (no overrides, no telescope rows, no conditional, no BUILDER target) — gets us to ~5-7× MapStruct without bytecode gen.
What I'm NOT proposing
Same NOT-list as the original post: no ASM / ByteBuddy / runtime bytecode generation. GraalVM native-image and the --release 17 cross-compile target stay intact.
Overnight Tier-D experiment run — resultsFour experiments shipped overnight, profile-led from a fresh ns-budget on the post-1.0 hot path. Runtime-path gap to MapStruct narrowed from ~78× (pre-Tier-C) to ~11.6× on deep tier — the tier where production code actually lives. Stacked results (Exp1 + Exp2 — both clean wins, compose orthogonally)
Per-experimentPR #123 — Exp1: Fuse src+remap — Eliminates the source-side PR #124 — Exp2: FORWARD_SEEN hoist for acyclic pairs — Skips the cycle-safe shell ThreadLocal probe on statically-acyclic type pairs. Cyclic SCCs still get the full guard. -30% nested fwd, -40% deep fwd. WAY bigger than predicted — the shell fires once per level walked, compounding in deep trees. PR #125 — Exp3: Stacked Exp1+Exp2 reference — confirms orthogonal composition. Skip merge; reference only. PR #126 — Exp4: All-identity short-circuit — INCONCLUSIVE. Wins on flat/nested fwd by 1-6 ns, regresses on nested bwd by +8 ns. Most likely cause: adding a second `Iso.of` call site polluted the JIT inline cache. Pushed as research artifact. Worth re-trying as a static factory. Updated Tier-D queue
Next moveMerge #123 and #124 separately to |
Closing — research charter completeEvery actionable tier from this thread landed:
Net effect: the runtime-path gap to MapStruct narrowed from ~78× → ~11.6× on the deep tier (the tier production code lives in), and the later assembly-time bean-reader capture (#175) pushed bean-side rows down further (~half the pre-capture cost). The canonical, living record of where the runtime path now stands — with the CI-reproducible matrix — is One carry-forward, not blocking: GraalVM native-image verification of the LMF Closing as resolved. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Goal
Close the gap between Telescope's runtime
Telescope.mapper(...)path and MapStruct's compile-time-generated bytecode.Where the gap actually lives — honest re-measurement
Earlier framing on PR #117 implied an 8× speedup from the fast-path. That comparison was apples-to-oranges (bean→record slow path vs record→record fast path). The honest comparison on identical fixtures (JDK 25, Apple Silicon, 2-fork × 5-warmup × 10-iter × 1 s):
¹ Mixed shape — fast-path tightly requires both sides be the same kind.
The 90× gap to MapStruct lives in the bean↔record / record↔bean rows. Record↔record was already cheap on the slow path because records have LMF-built readers + canonical-ctor spreader. Bean↔X is dominated by getter/setter reflection through the structural Iso + the
LinkedHashMapintermediate.Where the time goes (bean→record slow path, ~376 ns)
Bean getter dispatch + HashMap intermediate = ~80% of the cost. The Iso composition chain is ~10%.
Optimization candidates, priority ranked
Tier A — record↔record + bean↔bean fast-path (LANDED in #117)
Per-pair
Function<S, T>via cached LMF readers + canonical-ctor spreader. Skip structural Iso composition entirely for the pure-auto-recursion same-shape case.Measured: ~12% on record→record. Smaller than predicted because records were already cheap. Real value: eliminates the
LinkedHashMapintermediate for the eligible cases.Tier B — Iso composition fusion (LANDED in #117, no measurable speedup)
Fuse the 3-hop
srcReader.then(remap).then(tgtBuilder)chain into one Iso with two Function bodies. Architecturally cleaner (2 fewer anonymous Iso allocations per pair, 1 virtualIso.todispatch instead of 3). Per-call gain within noise (~3% on the 2-fork bench).Why it didn't move the needle: composition allocations only fire at build-time. JIT was already inlining the per-call chain reasonably.
Tier C —
LinkedHashMap→Object[]structural intermediate (NOT YET LANDED)This is the big remaining win. Replace the per-call
LinkedHashMap<String, Object>intermediate withObject[]indexed by component position. Per-classname → indextable cached atpopulateIsotime.Reflective.structuralIsoshape fromIso<S, Map<String, Object>>toIso<S, Object[]>. Touches all consumers inDeepMap. Real engineering.Tier D — minor wins (post-Tier C)
MethodHandlefor the Iso chain (probably negligible; the function references already inline)String.hashCodecaching (irrelevant once the map intermediate is gone)What we are explicitly NOT proposing
Runtime bytecode generation (ASM, ByteBuddy,
MethodHandles.classData). Would close the gap further but:--release 17cross-compile target breaks if we lean on newerMethodHandle.classDataAPIsThe Tier A–D approach uses only
LambdaMetafactory+ cachedFunction/Supplier— the same primitives already proven in:internal/Beans.javaand:internal/Records.java. Lattice-honest, no new deps.Recommendation
After Tier C, expected ~5-10× MapStruct on bean↔record at the flat tier. At that point the runtime path is "fast enough" for the vast majority of production services and
@Bridgecodegen remains the documented mitigation for hot paths.Open questions for design review
DeepMap.lazyCacheIso. Need a clean test that pins this.Mapping.to(...)row probably stays fast (just substitutes one reader); singleMapping.via(...)probably falls back (nested mapper composition). Needs spec.--initialize-at-build-timefor the Tier-C arity cache? Probably yes (matches existingBeans.javapattern) but unverified.All reactions