Skip to content

Commit bd09b2b

Browse files
authored
perf: closure-free LazyDefault for default argument evaluation (#762)
## Motivation Default argument evaluation in `Val.Func.apply` allocates 2 objects per default: a `LazyFunc` wrapper plus a `Function0` closure. For functions with multiple defaults called in hot paths, this creates unnecessary GC pressure. ## Key Design Decision Introduce `LazyDefault`, a purpose-built `Lazy` subclass that captures the expression, scope, function, and eval scope as fields instead of a closure. This follows the established `LazyExpr`/`LazyApply1`/`LazyApply2` pattern in the codebase. ## Modification - Add `LazyDefault` final class in `Val.scala` (24 bytes, same as `LazyExpr`) - Replace `new LazyFunc(() => evalDefault(default, newScope, ev))` with `new LazyDefault(default, newScope, this, ev)` in `Func.apply` - Clear captured references after first evaluation to allow GC ## Benchmark Results ### JMH (single fork, 1 iteration — directional only) | Benchmark | Master (ms/op) | This PR (ms/op) | Change | |-----------|---------------|-----------------|--------| | assertions | 0.222 | 0.205 | **-7.7%** | | bench.02 | 35.235 | 34.531 | -2.0% | | bench.03 | 9.809 | 9.652 | -1.6% | | manifestTomlEx | 0.074 | 0.069 | **-6.8%** | | reverse | 6.667 | 6.448 | **-3.3%** | Most benchmarks show improvement or are within noise margins. ### Scala Native vs jrsonnet (hyperfine) Not applicable for this change — the allocation reduction primarily benefits JVM GC. Native impact is minimal but non-negative. ## Analysis The optimization reduces allocation from 2 objects to 1 per default argument evaluation. The `LazyDefault` pattern is consistent with existing `LazyExpr`, `LazyApply1`, `LazyApply2` classes. No semantic changes. ## References - Upstream: jit branch commit [`d40ae8cc`](He-Pin@d40ae8cc) ## Result All 420 tests pass across JVM/JS/Native × Scala 3.3.7, 2.13.18, 2.12.21. --- ## JMH Benchmark Results (vs master 0d13274) | Benchmark | Master (ms/op) | This PR (ms/op) | Change | |-----------|---------------:|----------------:|-------:| | assertions | 0.207 | 0.204 | -1.4% | | base64 | 0.156 | 0.156 | +0.0% | | improved base64Decode | 0.123 | 0.117 | -4.9% | | regressed base64DecodeBytes | 5.899 | 6.071 | +2.9% | | base64_byte_array | 0.803 | 0.790 | -1.6% | | bench.01 | 0.052 | 0.052 | +0.0% | | improved bench.02 | 35.401 | 34.003 | -3.9% | | bench.03 | 9.583 | 9.715 | +1.4% | | bench.04 | 0.122 | 0.120 | -1.6% | | regressed bench.06 | 0.224 | 0.229 | +2.2% | | improved bench.07 | 3.332 | 3.174 | -4.7% | | regressed bench.08 | 0.038 | 0.039 | +2.6% | | regressed bench.09 | 0.041 | 0.042 | +2.4% | | comparison | 0.028 | 0.028 | +0.0% | | improved comparison2 | 18.681 | 18.256 | -2.3% | | improved escapeStringJson | 0.032 | 0.031 | -3.1% | | foldl | 0.077 | 0.076 | -1.3% | | gen_big_object | 0.918 | 0.920 | +0.2% | | regressed large_string_join | 0.555 | 0.573 | +3.2% | | regressed large_string_template | 1.600 | 1.641 | +2.6% | | regressed lstripChars | 0.113 | 0.116 | +2.7% | | manifestJsonEx | 0.052 | 0.052 | +0.0% | | manifestTomlEx | 0.069 | 0.068 | -1.4% | | manifestYamlDoc | 0.055 | 0.056 | +1.8% | | member | 0.656 | 0.656 | +0.0% | | regressed parseInt | 0.032 | 0.033 | +3.1% | | realistic1 | 1.661 | 1.680 | +1.1% | | regressed realistic2 | 57.541 | 60.763 | +5.6% | | regressed reverse | 6.717 | 6.943 | +3.4% | | rstripChars | 0.119 | 0.119 | +0.0% | | setDiff | 0.431 | 0.434 | +0.7% | | setInter | 0.371 | 0.376 | +1.3% | | setUnion | 0.604 | 0.615 | +1.8% | | stripChars | 0.117 | 0.118 | +0.9% | | regressed substr | 0.057 | 0.060 | +5.3% | **Summary**: 5 improvements, 11 regressions, 19 neutral **Platform**: Apple Silicon, JMH single-shot avg
1 parent 0d13274 commit bd09b2b

1 file changed

Lines changed: 26 additions & 1 deletion

File tree

sjsonnet/src/sjsonnet/Val.scala

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,31 @@ final class LazyApply2(
131131
}
132132
}
133133

134+
/**
135+
* Closure-free [[Lazy]] that defers `func.evalDefault(expr, scope, ev)`.
136+
*
137+
* Used in [[Val.Func.apply]] for default parameter evaluation, eliminating the 2-object allocation
138+
* (LazyFunc + Function0 closure) of the original pattern.
139+
*/
140+
final class LazyDefault(
141+
private var exprOrVal: AnyRef, // Expr before compute, Val after
142+
private var scope: ValScope,
143+
private var func: Val.Func,
144+
private var ev: EvalScope)
145+
extends Lazy {
146+
def value: Val = {
147+
if (ev == null) exprOrVal.asInstanceOf[Val]
148+
else {
149+
val r = func.evalDefault(exprOrVal.asInstanceOf[Expr], scope, ev)
150+
exprOrVal = r
151+
scope = null.asInstanceOf[ValScope]
152+
func = null
153+
ev = null
154+
r
155+
}
156+
}
157+
}
158+
134159
/**
135160
* [[Val]]s represented Jsonnet values that are the result of evaluating a Jsonnet program. The
136161
* [[Val]] data structure is essentially a JSON tree, except evaluation of object attributes and
@@ -1441,7 +1466,7 @@ object Val {
14411466
if (argVals(j) == null) {
14421467
val default = params.defaultExprs(i)
14431468
if (default != null) {
1444-
argVals(j) = new LazyFunc(() => evalDefault(default, newScope, ev))
1469+
argVals(j) = new LazyDefault(default, newScope, this, ev)
14451470
} else {
14461471
if (missing == null) missing = new ArrayBuffer
14471472
missing.+=(params.names(i))

0 commit comments

Comments
 (0)