Skip to content

perf: fast-path Num stringify in OP_+ string concatenation#684

Open
He-Pin wants to merge 1 commit intodatabricks:masterfrom
He-Pin:perf/num-stringify-fastpath
Open

perf: fast-path Num stringify in OP_+ string concatenation#684
He-Pin wants to merge 1 commit intodatabricks:masterfrom
He-Pin:perf/num-stringify-fastpath

Conversation

@He-Pin
Copy link
Copy Markdown
Contributor

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

Motivation

When concatenating a number with a string (num + str or str + num), the evaluator calls Materializer.stringify() which performs a full type-dispatch pattern match on the Val just to extract the double for rendering. For Val.Num, this dispatch is unnecessary — we can call RenderUtils.renderDouble() directly.

This is particularly impactful for string template operations that concatenate many numbers with strings, such as building JSON-like output via string interpolation.

Key Design Decision

Add specific match cases for (Val.Num, Val.Str) and (Val.Str, Val.Num) in the OP_+ dispatch that call RenderUtils.renderDouble(n.asDouble) directly instead of going through Materializer.stringify().

Uses n.asDouble (not the raw destructured double from pattern match) to preserve the NaN guard in Val.Num.asDouble — ensuring identical error behavior to Materializer.stringify() for edge cases like (0 % 0) + "text".

Modification

Evaluator.scala — Added 2 lines in OP_+ dispatch:

case (n: Val.Num, Val.Str(_, r)) => Val.Str(pos, RenderUtils.renderDouble(n.asDouble) + r)
case (Val.Str(_, l), n: Val.Num) => Val.Str(pos, l + RenderUtils.renderDouble(n.asDouble))

Placed between (Str, Str) and generic (Str, any) cases to match numbers before the more expensive Materializer.stringify() fallback.

Benchmark Results

JMH (35 benchmarks, single-threaded)

Benchmark Before (ms/op) After (ms/op) Change
large_string_template 2.265 2.222 -1.9%
large_string_join 2.099 2.038 -2.9%

Zero regressions across all 35 benchmarks.

Native (hyperfine, 10 runs) 🔥

Benchmark Before After jrsonnet vs jrsonnet
large_string_template 17.8ms 11.8ms 5.6ms 2.12x slower (was 3.20x)
comparison2 170.8ms 174.2ms 237ms 1.36x faster
realistic2 295.9ms 294.4ms 483ms 1.64x faster

Analysis

  • Massive native improvement: -33.7% on large_string_template — the gap with jrsonnet narrowed from 3.20x to 2.12x
  • JVM improvement is modest (-1.9%) because JIT already partially inlines the stringify dispatch
  • Native benefit is much larger because native binary cannot inline across virtual dispatch boundaries as aggressively
  • The NaN guard via n.asDouble adds negligible overhead (one isNaN check on a hot-path double) while preserving correctness

References

Result

All 140 tests pass. Zero regressions. NaN error semantics preserved via asDouble guard.

Add direct RenderUtils.renderDouble() call for Num+Str and Str+Num
cases in binary OP_+ to avoid Materializer.stringify() dispatch overhead.

stringify() performs a full pattern match on Val type just to extract
the double for rendering. The direct call skips this dispatch entirely,
which is significant for string template operations that concatenate
many numbers with strings.

Uses n.asDouble (not raw destructured double) to preserve the NaN guard
that exists in Val.Num.asDouble — this ensures consistency with
Materializer.stringify() error behavior for not-a-number values.

Upstream: jit branch commit 4b1cd03
@He-Pin He-Pin marked this pull request as ready for review April 5, 2026 10:33
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