Skip to content

perf: compact compare() method for better JIT inlining#725

Merged
stephenamar-db merged 1 commit intodatabricks:masterfrom
He-Pin:fix/comparison-regression
Apr 10, 2026
Merged

perf: compact compare() method for better JIT inlining#725
stephenamar-db merged 1 commit intodatabricks:masterfrom
He-Pin:fix/comparison-regression

Conversation

@He-Pin
Copy link
Copy Markdown
Contributor

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

Motivation

The compare() method was changed to nested match in #691 to avoid Tuple2 allocation. However, this made the method body significantly larger, reducing JIT inlining effectiveness — especially in full-suite benchmark scenarios where many hot methods compete for the JVM inlining budget.

Key Design Decision

Revert to tuple match for the outer dispatch. Verified via javap that Scala 2.13+ pattern matcher lowers (x, y) match { case (X, Y) => ... } to direct instanceof/checkcast without any Tuple2 allocation at bytecode level. This gives us the same zero-allocation benefit as nested match, but with a much more compact method body that the JIT can inline and optimize better.

Modification

  1. Tuple match for compact bytecode — outer compare() dispatch uses (x, y) match pattern (no Tuple2 at bytecode level, verified)
  2. Extracted compareTypeMismatch() helper — moves error path out of the hot method to reduce bytecode size
  3. Reordered cases — Num/Str/Arr first (most common in benchmarks), Bool/Null last
  4. Preserved inner-loop optimizations — post-force reference equality check and inline numeric fast path in array comparison loop

Benchmark Results

JMH Full-Suite Results (Apple M4, JDK 24, @fork(1) @WarmUp(1) @measurement(1))

Key benchmarks showing improvement:

Benchmark Before (ms/op) After (ms/op) Change
comparison 25.659 21.470 -16.3%
comparison2 38.483 35.650 -7.4%
realistic2 73.594 66.265 -10.0%
reverse 12.496 10.428 -16.6%
bench.03 12.889 12.324 -4.4%

No regressions observed across the full suite (35 benchmarks).

Full JMH Results (After)
Benchmark Score (ms/op)
assertions 0.221
bench.01 0.055
bench.02 39.666
bench.03 12.324
bench.04 0.429
bench.06 0.298
bench.07 3.115
bench.08 0.040
bench.09 0.047
gen_big_object 1.002
large_string_join 2.052
large_string_template 1.813
realistic1 2.055
realistic2 66.265
base64 0.507
base64Decode 0.380
base64DecodeBytes 8.752
base64_byte_array 1.145
comparison 21.470
comparison2 35.650
escapeStringJson 0.031
foldl 0.249
lstripChars 0.375
manifestJsonEx 0.053
manifestTomlEx 0.069
manifestYamlDoc 0.056
member 0.667
parseInt 0.033
reverse 10.428
rstripChars 0.373
stripChars 0.362
substr 0.107
setDiff 0.425
setInter 0.377
setUnion 0.701

Analysis

The improvement is primarily from reduced JIT profile pollution. When the full benchmark suite runs, HotSpot must compile and optimize many different methods. A smaller compare() method body gives the JIT compiler more flexibility to inline and optimize it, even after many other methods have been compiled.

The improvement is most visible in:

  • Array-heavy benchmarks (comparison, comparison2, reverse) — these call compare() or related array operations frequently
  • Complex evaluation benchmarks (realistic2, bench.03) — benefit from better overall JIT decisions in Evaluator

Bytecode verification confirms zero Tuple2 allocation:

# javap output shows direct instanceof/checkcast, no new scala.Tuple2
public int compare(sjsonnet.Val, sjsonnet.Val);
  Code:
    0: aload_1
    1: instanceof    sjsonnet/Val$Num
    4: ifeq          38
    ...

References

Result

  • All tests pass (./mill sjsonnet.jvm[3.3.7].test)
  • Formatting verified (./mill sjsonnet.jvm[3.3.7].checkFormat)
  • No semantic changes — preserves all error handling and comparison behavior

Refactor compare() from nested match to tuple match with extracted error
helper. Scala 2.13+ pattern matcher lowers tuple patterns to direct
instanceof/checkcast without Tuple2 allocation (verified via javap).

The compact method body improves JIT inlining decisions, especially in
full-suite scenarios where many hot methods compete for inlining budget.

Key changes:
- Tuple match for compact bytecode (no Tuple2 at bytecode level)
- Extract compareTypeMismatch() to keep happy path small
- Reorder cases: Num/Str/Arr first (most common in benchmarks)
- Preserve post-force reference equality and numeric fast path in array loop
@He-Pin He-Pin marked this pull request as ready for review April 10, 2026 06:16
@He-Pin
Copy link
Copy Markdown
Contributor Author

He-Pin commented Apr 10, 2026

@stephenamar-db This works better on 2.13.x and the tuple2 allocation is been fixed in newer scala 3 release too @noti0na1

@He-Pin He-Pin marked this pull request as draft April 10, 2026 06:25
@He-Pin He-Pin marked this pull request as ready for review April 10, 2026 10:06
@stephenamar-db stephenamar-db merged commit f459b10 into databricks:master Apr 10, 2026
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.

2 participants