Skip to content

Commit a17ec44

Browse files
authored
perf: optimize sort allocation paths in std.sort and set operations (#752)
## Motivation `std.sort` and set operations (`std.setDiff`, `std.setInter`, `std.setUnion`) internally sort arrays but use allocation-heavy patterns: `.map()` for forcing lazy values, `.map().sortBy()` creating intermediate copies, and allocating a new `Array(1)` per key function call. ## Key Design Decisions - **Keep in-place Comparator sort for numerics** rather than primitive double sort + reconstruction. While primitive `Arrays.sort(double[])` is faster on JVM, the `Val.cachedNum` reconstruction step creates GC pressure on Scala Native (measured 1.26x regression). In-place Comparator sort avoids any reconstruction. - **Reuse argument buffer** for key function calls: single `Array[Val](1)` shared across all iterations instead of `Array(v.value)` per element. - **While-loops over `.map()`**: eliminates closure allocation, iterator overhead, and intermediate array copies. ## Modifications `sjsonnet/src/sjsonnet/stdlib/SetModule.scala`: 1. **Key function path**: Reuse single-element `argBuf` across all key function calls, use while-loop for key computation 2. **Result construction**: Pre-allocated `Array[Eval]` with while-loop instead of `sortedIndices.map(i => vs(i))` 3. **Strict force**: `while` loop with pre-allocated `Array[Val]` instead of `vs.map(_.value)` 4. **String sort (no key)**: In-place `Arrays.sort` with Comparator instead of `.map(_.cast[Val.Str]).sortBy(_.asString)` (2 intermediate array copies) 5. **Array sort (no key)**: In-place `Arrays.sort` with Comparator instead of `.map(_.cast[Val.Arr]).sortBy(identity)` (2 intermediate copies) ## Benchmark Results ### JMH (JVM, single iteration) | Benchmark | Before (ms/op) | After (ms/op) | Change | |-----------|----------------|---------------|--------| | bench.06 (sort) | 0.359 | 0.251 | **-30.1%** | | setDiff | 0.533 | 0.446 | **-16.3%** | | setInter | 0.367 | 0.386 | neutral | | setUnion | 0.727 | 0.677 | **-6.9%** | ### Scala Native hyperfine (`-N --warmup 10`) | Benchmark | Before (ms) | After (ms) | Speedup | |-----------|-------------|------------|---------| | bench.06 (sort, 30 runs) | 7.6 ± 0.4 | 5.5 ± 0.2 | **1.39x** | | setDiff (20 runs) | 8.8 ± 0.6 | 7.7 ± 0.6 | **1.13x** | | setInter (20 runs) | 8.6 ± 1.3 | 8.3 ± 0.8 | neutral | ## Analysis The allocation reduction benefits both JVM and Native, but the impact is more pronounced on Native where GC overhead is higher. The sort benchmark sees the largest improvement because it exercises all the optimized paths (force + sort + result construction). Set operations see moderate improvement since the merge-based intersection/difference only calls sort once on already-sorted inputs. ## References - Upstream exploration: he-pin/sjsonnet jit branch [`b1f64df0`](He-Pin@b1f64df0) ## Result Sort and set operations are faster on both JVM and Scala Native with zero semantic changes. All existing tests pass.
1 parent b35cde7 commit a17ec44

1 file changed

Lines changed: 41 additions & 10 deletions

File tree

sjsonnet/src/sjsonnet/stdlib/SetModule.scala

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,16 @@ object SetModule extends AbstractFunctionModule {
9292
Val.Arr(
9393
pos,
9494
if (keyFFunc != null) {
95-
val keys: Array[Val] = vs.map(v =>
96-
keyFFunc(Array(v.value), null, pos.noOffset)(ev, TailstrictModeDisabled).value
97-
)
95+
// Reuse a single-element argument buffer across all key function calls
96+
// to avoid allocating a new Array(1) per element.
97+
val keys = new Array[Val](vs.length)
98+
val argBuf = new Array[Val](1)
99+
var i = 0
100+
while (i < vs.length) {
101+
argBuf(0) = vs(i).value
102+
keys(i) = keyFFunc(argBuf, null, pos.noOffset)(ev, TailstrictModeDisabled).value
103+
i += 1
104+
}
98105
val keyType = keys(0).getClass
99106
if (classOf[Val.Bool].isAssignableFrom(keyType)) {
100107
Error.fail("Cannot sort with key values that are booleans")
@@ -122,21 +129,42 @@ object SetModule extends AbstractFunctionModule {
122129
Error.fail("Cannot sort with key values that are " + keys(0).prettyName + "s")
123130
}
124131

125-
sortedIndices.map(i => vs(i))
132+
// Use while-loop instead of .map() to avoid closure + iterator allocation
133+
val result = new Array[Eval](sortedIndices.length)
134+
var j = 0
135+
while (j < sortedIndices.length) {
136+
result(j) = vs(sortedIndices(j))
137+
j += 1
138+
}
139+
result
126140
} else {
127-
val strict = vs.map(_.value)
141+
// Force all lazy elements to strict values using while-loop
142+
val strict = new Array[Val](vs.length)
143+
var i = 0
144+
while (i < vs.length) {
145+
strict(i) = vs(i).value
146+
i += 1
147+
}
128148
val keyType = strict(0).getClass
129149
if (classOf[Val.Bool].isAssignableFrom(keyType)) {
130150
Error.fail("Cannot sort with values that are booleans")
131151
}
132152
if (!strict.forall(_.getClass == keyType))
133153
Error.fail("Cannot sort with values that are not all the same type")
134154

155+
// Sort in-place to avoid intermediate array allocations
135156
if (keyType == classOf[Val.Str]) {
136-
strict.map(_.cast[Val.Str]).sortBy(_.asString)(Util.CodepointStringOrdering)
157+
java.util.Arrays.sort(
158+
strict.asInstanceOf[Array[AnyRef]],
159+
(a: AnyRef, b: AnyRef) =>
160+
Util.compareStringsByCodepoint(
161+
a.asInstanceOf[Val.Str].asString,
162+
b.asInstanceOf[Val.Str].asString
163+
)
164+
)
137165
} else if (keyType == classOf[Val.Num]) {
138-
// In-place sort: avoids the two intermediate array copies from
139-
// .map(_.cast[Val.Num]).sortBy(_.asDouble). Uses TimSort (stable)
166+
// In-place sort using Comparator: avoids extracting + reconstructing Val.Num
167+
// objects which causes GC pressure on Scala Native. TimSort (stable) is used,
140168
// which is excellent for nearly-sorted inputs (common for std.range).
141169
java.util.Arrays.sort(
142170
strict.asInstanceOf[Array[AnyRef]],
@@ -146,14 +174,17 @@ object SetModule extends AbstractFunctionModule {
146174
b.asInstanceOf[Val.Num].asDouble
147175
)
148176
)
149-
strict
150177
} else if (keyType == classOf[Val.Arr]) {
151-
strict.map(_.cast[Val.Arr]).sortBy(identity)(ev.compare(_, _))
178+
java.util.Arrays.sort(
179+
strict.asInstanceOf[Array[AnyRef]],
180+
(a: AnyRef, b: AnyRef) => ev.compare(a.asInstanceOf[Val.Arr], b.asInstanceOf[Val.Arr])
181+
)
152182
} else if (keyType == classOf[Val.Obj]) {
153183
Error.fail("Unable to sort array of objects without key function")
154184
} else {
155185
Error.fail("Cannot sort array of " + strict(0).prettyName)
156186
}
187+
strict
157188
}
158189
)
159190
}

0 commit comments

Comments
 (0)