Skip to content

perf: valTag dispatch for O(1) materializer type routing#682

Open
He-Pin wants to merge 1 commit intodatabricks:masterfrom
He-Pin:perf/valtag-dispatch
Open

perf: valTag dispatch for O(1) materializer type routing#682
He-Pin wants to merge 1 commit intodatabricks:masterfrom
He-Pin:perf/valtag-dispatch

Conversation

@He-Pin
Copy link
Copy Markdown
Contributor

@He-Pin He-Pin commented Apr 5, 2026

Motivation

The materializeRecursiveChild method in Materializer.scala dispatches on Val subtype using a Scala pattern match, which compiles to sequential instanceof checks. For 8 concrete Val subtypes, this means up to 7 type tests before reaching the correct branch. This is the hot path for all JSON materialization.

Key Design Decision

Introduce a valTag: Byte field on each Val subclass with contiguous values 0-7, enabling @switch annotation to generate JVM tableswitch bytecode for O(1) dispatch. Verified with javap -c that the bytecode is:

tableswitch { // 0 to 7
  0: ...  // TAG_STR
  1: ...  // TAG_NUM
  ...
  7: ...  // TAG_FUNC
  default: ...  // Materializable, TailCall, unknown
}

Custom Val subclasses (e.g. Materializable) use valTag = -1 and fall through to a nested pattern match in the default branch.

Modification

  • Val.scala: Added private[sjsonnet] def valTag: Byte abstract method to sealed abstract class Val. Added TAG constants (TAG_STR=0 through TAG_FUNC=7). Every concrete subclass overrides valTag.
  • Materializer.scala: Replaced pattern-match dispatch in materializeRecursiveChild with @switch tableswitch dispatch. Added null guard before valTag access to preserve original diagnostic. Hoisted xs.length out of materializeRecursiveArr while-loop.
  • CustomValTests.scala: Added valTag: Byte = -1 to test ImportantString class.

Benchmark Results

JMH (35 benchmarks, single-threaded)

Benchmark Before (ms/op) After (ms/op) Change
reverse 10.705 10.410 -2.8%
base64DecodeBytes 9.423 8.990 -4.6%
comparison2 74.108 72.667 -1.9%
realistic2 70.491 69.201 -1.8%

Zero regressions across all 35 benchmarks.

Native (hyperfine, 10 runs)

Benchmark Before After jrsonnet vs jrsonnet
comparison2 170.8ms 171.2ms 237ms 1.39x faster
realistic2 295.9ms 294.1ms 483ms 1.64x faster
reverse 49.6ms 50.1ms 33.6ms 1.49x slower

Native impact is neutral (within noise) — the primary value is architectural: tableswitch enables future materializer optimizations to benefit from O(1) dispatch.

Analysis

  • JVM sees modest improvement (2-5%) because JIT can leverage the tableswitch more efficiently than cascading instanceof
  • Native binary sees neutral impact because Scala Native already optimizes pattern matching well
  • The primary value is architectural: O(1) dispatch is a foundation for future materializer fast-paths

References

Result

All 140 tests pass. @switch tableswitch bytecode verified via javap -c. No regressions.

Add a valTag: Byte abstract method to Val with TAG constants (0-7) for
each concrete subclass, enabling JVM tableswitch O(1) dispatch in the
materializer instead of linear pattern matching.

Changes:
- Val.scala: Add valTag abstract method and TAG_STR/NUM/TRUE/FALSE/NULL/
  ARR/OBJ/FUNC constants (0-7 contiguous range)
- Materializer.scala: Replace pattern-match in materializeRecursiveChild
  with @switch tableswitch dispatch on valTag. Hoist xs.length out of
  materializeRecursiveArr while-loop.
- CustomValTests.scala: Add valTag=-1 to ImportantString (custom Val)

JMH improvements: reverse -2.9%, base64DecodeBytes -4.6%, comparison2 -1.9%,
base64 -2.1%. No regressions outside noise range.

Upstream: he-pin/sjsonnet jit branch commits 30b7495, 9ddb1a5
@He-Pin He-Pin marked this pull request as ready for review April 5, 2026 08:34
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.

1 participant