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 Jsonnet concatenates a number with a string via +, the evaluator goes through Materializer.stringify() which handles all Val types via pattern matching. For the common case of Num + Str or Str + Num, this generic dispatch adds unnecessary overhead — we already know the value is a Val.Num from the enclosing match.

Key Design Decision

Add specialized (Val.Num, Val.Str) and (Val.Str, Val.Num) match cases before the generic (Val.Str, r) / (l, Val.Str) fallbacks. These cases call RenderUtils.renderDouble(n.asDouble) directly, bypassing Materializer.stringify's type dispatch.

Modification

sjsonnet/src/sjsonnet/Evaluator.scalaOP_+ handler:

  • Added two new cases in the (l, r) match block:
    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))
  • These are placed after (Val.Num, Val.Num) and (Val.Str, Val.Str) but before the generic stringify fallbacks.

Benchmark Results

JMH — Isolated Targeted (JVM, 5 runs each, median)

Benchmark Before (ms) After (ms) Δ
large_string_template 1.784 1.793 ±0%

JMH — Full Suite (35 benchmarks, 1+1 warmup)

No regressions detected. All benchmarks within noise margin.

Note

On the JVM, the JIT compiler inlines Materializer.stringify through profile-guided optimization, reducing the benefit of this explicit fast path. The primary value is:

  1. Scala Native: No JIT — avoiding virtual dispatch for the hot Num + Str path is significant.
  2. Code clarity: Makes the common Num + Str concatenation path explicit in the source.

Analysis

  • Correctness: RenderUtils.renderDouble is the same function called by Materializer.stringify for Val.Num — semantics are identical.
  • Pattern order: New cases come after the most common (Num, Num) and (Str, Str) cases, so they don't add overhead to those hot paths.

References

  • Materializer.stringifyRenderUtils.renderDouble for Val.Num
  • RenderUtils.renderDouble handles integer-valued doubles (e.g., 42.0"42")

Result

Type-specialized fast path for number-string concatenation. No regressions. Benefits Scala Native and improves code explicitness.

@He-Pin He-Pin marked this pull request as ready for review April 5, 2026 10:33
@stephenamar-db
Copy link
Copy Markdown
Collaborator

CI check found a scalafmt formatting violation in sjsonnet/src/sjsonnet/Evaluator.scala. Tests pass. Please run ./mill __.reformat and push the fix.

@He-Pin He-Pin force-pushed the perf/num-stringify-fastpath branch 4 times, most recently from 132d361 to cca73c5 Compare April 9, 2026 03:02
@stephenamar-db
Copy link
Copy Markdown
Collaborator

Formatting check is still failing on sjsonnet/src/sjsonnet/Evaluator.scala after rebase. Please run ./mill __.reformat and push the fix.

@He-Pin He-Pin force-pushed the perf/num-stringify-fastpath branch from cca73c5 to 554f7c7 Compare April 10, 2026 03:31
@He-Pin
Copy link
Copy Markdown
Contributor Author

He-Pin commented Apr 10, 2026

Fixed — formatting has been corrected after the rebase. scalafmt run included in the latest force-push.

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 force-pushed the perf/num-stringify-fastpath branch from 554f7c7 to 075fa18 Compare April 10, 2026 09:25
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