From 5e29bf068d9afb0fd7ef7f80fd25eb6ff9c18062 Mon Sep 17 00:00:00 2001 From: He-Pin Date: Wed, 15 Apr 2026 20:05:38 +0800 Subject: [PATCH 1/2] perf: O(1) sort and equality for range arrays Motivation: std.sort on a range array (e.g. std.range(1, 1000)) was materializing the entire range into an Eval[] array and performing O(n log n) sort on already-sorted data. Similarly, comparing two range arrays with std.assertEqual did O(n) element-by-element comparison even when both ranges describe the same integer sequence. Modification: - Val.Arr.asSortedIfKnown: returns the range as-is for forward ranges, or the O(1) reversed equivalent for reversed ranges. Returns null for non-range arrays to fall through to full sort. - Val.Arr.rangeEquals: O(1) structural equality for two ranges by comparing (rangeFrom, length, reversed) fields directly. - SetModule.sortArr: checks asSortedIfKnown before materializing. - Evaluator.equal: adds reference equality short-circuit (x eq y) and rangeEquals fast path before element-by-element comparison. Result: array_sorts benchmark improved from 8.3ms to 7.7ms (master to PR). No regressions on realistic_2 or other benchmarks. --- sjsonnet/src/sjsonnet/Evaluator.scala | 2 ++ sjsonnet/src/sjsonnet/Val.scala | 20 ++++++++++++++++++++ sjsonnet/src/sjsonnet/stdlib/SetModule.scala | 16 +++++++++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/sjsonnet/src/sjsonnet/Evaluator.scala b/sjsonnet/src/sjsonnet/Evaluator.scala index fd02cb4f..ab077d56 100644 --- a/sjsonnet/src/sjsonnet/Evaluator.scala +++ b/sjsonnet/src/sjsonnet/Evaluator.scala @@ -2012,8 +2012,10 @@ class Evaluator( case x: Val.Arr => y match { case y: Val.Arr => + if (x eq y) return true val xlen = x.length if (xlen != y.length) return false + if (x.rangeEquals(y)) return true // Skip shared ConcatView prefix — elements are reference-identical var i = x.sharedConcatPrefixLength(y) while (i < xlen) { diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index c717c93c..9b7b7851 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -647,6 +647,26 @@ object Val { true } + /** + * If this array is known to be sorted in ascending numeric order (e.g. a forward range), return + * it directly. If it is a reversed range, return the equivalent forward range in O(1). Returns + * null if sort order is unknown and a full sort is needed. + */ + private[sjsonnet] def asSortedIfKnown(newPos: Position): Arr = { + if (isRange) { + if (_reversed) reversed(newPos) else this + } else null + } + + /** + * O(1) equality check for range arrays. Two ranges are equal if they describe the same integer + * sequence (same start, length, and direction). Returns false if either array is not a range. + */ + private[sjsonnet] def rangeEquals(other: Arr): Boolean = { + isRange && other.isRange && _length == other._length && + _reversed == other._reversed && _rangeFrom == other._rangeFrom + } + /** * Create a reversed view of this array without copying. The returned Arr shares the same * backing array but flips the reversed flag, so element access is O(1) with zero allocation. diff --git a/sjsonnet/src/sjsonnet/stdlib/SetModule.scala b/sjsonnet/src/sjsonnet/stdlib/SetModule.scala index e78b9fe7..be91c758 100644 --- a/sjsonnet/src/sjsonnet/stdlib/SetModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/SetModule.scala @@ -82,7 +82,21 @@ object SetModule extends AbstractFunctionModule { Val.Arr(pos, out.result()) } - private def sortArr(pos: Position, ev: EvalScope, arr: Val, keyF: Val) = { + private def sortArr(pos: Position, ev: EvalScope, arr: Val, keyF: Val): Val = { + // Fast path: range arrays are already sorted ascending by construction. + // Avoids O(n) materialization + O(n log n) sort for already-sorted data. + if (keyF == null || keyF.isInstanceOf[Val.False]) { + arr match { + case a: Val.Arr => + val sorted = a.asSortedIfKnown(pos) + if (sorted != null) return sorted + case _ => + } + } + sortArrSlow(pos, ev, arr, keyF) + } + + private def sortArrSlow(pos: Position, ev: EvalScope, arr: Val, keyF: Val) = { val vs = toArrOrString(arr, pos, ev) if (vs.length <= 1) { arr From 8fb5228327d846d0d7669a6957b97526aa8ff9e3 Mon Sep 17 00:00:00 2001 From: He-Pin Date: Sat, 25 Apr 2026 16:36:17 +0800 Subject: [PATCH 2/2] fix: replace undefined isRange with RangeArr instanceof checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After rebase onto master, isRange field was not defined in Arr class. Use pattern matching on RangeArr subclass instead. 🤖 Generated with [Qoder][https://qoder.com] --- sjsonnet/src/sjsonnet/Val.scala | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index 9b7b7851..526a79f2 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -652,20 +652,21 @@ object Val { * it directly. If it is a reversed range, return the equivalent forward range in O(1). Returns * null if sort order is unknown and a full sort is needed. */ - private[sjsonnet] def asSortedIfKnown(newPos: Position): Arr = { - if (isRange) { - if (_reversed) reversed(newPos) else this - } else null + private[sjsonnet] def asSortedIfKnown(newPos: Position): Arr = this match { + case _: RangeArr => if (_reversed) reversed(newPos) else this + case _ => null } /** * O(1) equality check for range arrays. Two ranges are equal if they describe the same integer * sequence (same start, length, and direction). Returns false if either array is not a range. */ - private[sjsonnet] def rangeEquals(other: Arr): Boolean = { - isRange && other.isRange && _length == other._length && - _reversed == other._reversed && _rangeFrom == other._rangeFrom - } + private[sjsonnet] def rangeEquals(other: Arr): Boolean = + (this, other) match { + case (r1: RangeArr, r2: RangeArr) => + r1._length == r2._length && r1._reversed == r2._reversed && r1.rangeFrom == r2.rangeFrom + case _ => false + } /** * Create a reversed view of this array without copying. The returned Arr shares the same @@ -691,7 +692,7 @@ object Val { * * Inspired by jrsonnet's RangeArray (arr/spec.rs). */ - final class RangeArr(pos0: Position, private val rangeFrom: Int, size: Int) + final class RangeArr(pos0: Position, private[sjsonnet] val rangeFrom: Int, size: Int) extends Arr(pos0, null) { _length = size