Skip to content

Commit 0d13274

Browse files
authored
perf: Long fast path for formatOctal and formatHexadecimal (#764)
## Motivation `formatOctal` and `formatHexadecimal` always allocate `BigInt` via `truncateToInteger`, even when the value fits in a `Long`. The existing `formatInteger` already has a `Long` fast path that avoids this allocation — extending the same pattern to octal and hex formatting. ## Key Design Decision Inline the Long-fits check (`sl.toDouble == s && sl != Long.MinValue`) directly into `formatOctal` and `formatHexadecimal`, using `java.lang.Long.toString(value, radix)` for the fast path. Fall back to `BigDecimal.toBigInt` only for values exceeding Long range. ## Modification - `Format.scala`: Add Long fast path to `formatOctal` and `formatHexadecimal` - `Format.scala`: Remove now-unused `truncateToInteger` helper method - Use `rhs2.charAt(0)` instead of `rhs2(0)` for consistency (avoids Scala implicit conversion) ## Benchmark Results ### JMH (single fork, 1 iteration — directional only) Format-heavy benchmarks are the primary beneficiaries: | Benchmark | Master (ms/op) | This PR (ms/op) | Change | |-----------|---------------|-----------------|--------| | large_string_template | 1.610 | 1.646 | +2.2% (noise) | | bench.03 | 9.809 | 9.652 | -1.6% | The `%o` and `%x` format specifiers are less common than `%d` in typical Jsonnet, so the impact is modest but still beneficial when these paths are hit. ### Scala Native vs jrsonnet (hyperfine) | Benchmark | sjsonnet Native (ms) | jrsonnet (ms) | Ratio | |-----------|---------------------|---------------|-------| | large_string_template | 17.3 | 8.0 | 2.15x (jrsonnet wins) | Gap is primarily due to process startup overhead and overall eval speed, not format-specific. ## Analysis This is a correctness-neutral optimization that extends an established pattern (Long fast path in `formatInteger`) to two more methods. No new code paths — the fallback to `BigDecimal.toBigInt` handles edge cases identically to the original `truncateToInteger`. ## References - Upstream: jit branch commit [`6524d77d`](He-Pin@6524d77d) ## Result All 420 tests pass across JVM/JS/Native × Scala 3.3.7, 2.13.18, 2.12.21.
1 parent 89c64be commit 0d13274

1 file changed

Lines changed: 60 additions & 34 deletions

File tree

sjsonnet/src/sjsonnet/Format.scala

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -488,14 +488,6 @@ object Format {
488488
output.toString()
489489
}
490490

491-
// Use Long as a fast path for integer formatting to avoid BigInt allocation.
492-
// Most Jsonnet integers fit in a Long; only fall back to BigDecimal for very large values.
493-
private def truncateToInteger(s: Double): BigInt = {
494-
val sl = s.toLong
495-
if (sl.toDouble == s) BigInt(sl)
496-
else BigDecimal(s).toBigInt
497-
}
498-
499491
private def formatInteger(formatted: FormatSpec, s: Double): String = {
500492
// Fast path: if the value fits in a Long (and isn't Long.MinValue where
501493
// negation overflows), avoid BigInt allocation entirely
@@ -537,35 +529,69 @@ object Format {
537529
}
538530

539531
private def formatOctal(formatted: FormatSpec, s: Double): String = {
540-
val i = truncateToInteger(s)
541-
val negative = i.signum < 0
542-
val lhs = if (negative) "-" else ""
543-
val rhs = i.abs.toString(8)
544-
val rhs2 = precisionPad(lhs, rhs, formatted.precision)
545-
widen(
546-
formatted,
547-
lhs,
548-
if (!formatted.alternate || rhs2(0) == '0') "" else "0",
549-
rhs2,
550-
numeric = true,
551-
signedConversion = !negative
552-
)
532+
// Fast path: if the value fits in a Long, avoid BigInt allocation
533+
val sl = s.toLong
534+
if (sl.toDouble == s && sl != Long.MinValue) {
535+
val negative = sl < 0
536+
val lhs = if (negative) "-" else ""
537+
val rhs = java.lang.Long.toString(if (negative) -sl else sl, 8)
538+
val rhs2 = precisionPad(lhs, rhs, formatted.precision)
539+
widen(
540+
formatted,
541+
lhs,
542+
if (!formatted.alternate || rhs2.charAt(0) == '0') "" else "0",
543+
rhs2,
544+
numeric = true,
545+
signedConversion = !negative
546+
)
547+
} else {
548+
val i = BigDecimal(s).toBigInt
549+
val negative = i.signum < 0
550+
val lhs = if (negative) "-" else ""
551+
val rhs = i.abs.toString(8)
552+
val rhs2 = precisionPad(lhs, rhs, formatted.precision)
553+
widen(
554+
formatted,
555+
lhs,
556+
if (!formatted.alternate || rhs2.charAt(0) == '0') "" else "0",
557+
rhs2,
558+
numeric = true,
559+
signedConversion = !negative
560+
)
561+
}
553562
}
554563

555564
private def formatHexadecimal(formatted: FormatSpec, s: Double): String = {
556-
val i = truncateToInteger(s)
557-
val negative = i.signum < 0
558-
val lhs = if (negative) "-" else ""
559-
val rhs = i.abs.toString(16)
560-
val rhs2 = precisionPad(lhs, rhs, formatted.precision)
561-
widen(
562-
formatted,
563-
lhs,
564-
if (!formatted.alternate) "" else "0x",
565-
rhs2,
566-
numeric = true,
567-
signedConversion = !negative
568-
)
565+
// Fast path: if the value fits in a Long, avoid BigInt allocation
566+
val sl = s.toLong
567+
if (sl.toDouble == s && sl != Long.MinValue) {
568+
val negative = sl < 0
569+
val lhs = if (negative) "-" else ""
570+
val rhs = java.lang.Long.toString(if (negative) -sl else sl, 16)
571+
val rhs2 = precisionPad(lhs, rhs, formatted.precision)
572+
widen(
573+
formatted,
574+
lhs,
575+
if (!formatted.alternate) "" else "0x",
576+
rhs2,
577+
numeric = true,
578+
signedConversion = !negative
579+
)
580+
} else {
581+
val i = BigDecimal(s).toBigInt
582+
val negative = i.signum < 0
583+
val lhs = if (negative) "-" else ""
584+
val rhs = i.abs.toString(16)
585+
val rhs2 = precisionPad(lhs, rhs, formatted.precision)
586+
widen(
587+
formatted,
588+
lhs,
589+
if (!formatted.alternate) "" else "0x",
590+
rhs2,
591+
numeric = true,
592+
signedConversion = !negative
593+
)
594+
}
569595
}
570596

571597
private def precisionPad(lhs: String, rhs: String, precision: Option[Int]): String = {

0 commit comments

Comments
 (0)