From 8a2e725e1911c09cb650b6c5aecbe231c2ce5df5 Mon Sep 17 00:00:00 2001 From: He-Pin Date: Mon, 6 Apr 2026 20:06:32 +0800 Subject: [PATCH] perf: optimize std.makeArray for constant-body functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When std.makeArray receives a function whose body is a constant literal (e.g. function(_) 'x'), skip per-element LazyApply1 thunk allocation and Val.Num index creation. Instead, fill the entire array with the constant value using java.util.Arrays.fill. Safety guards: - Only applies when func.params.names.length == 1 (preserves arity checks) - Only applies when bodyExpr is a Val.Literal (Str/Num/Bool/Null/Arr/Obj) - bodyExpr is only set by Evaluator.visitMethod, builtins default to null Benchmark: -61.8% on large_string_join (1.902 → 0.726 ms/op) Upstream: jit branch commit 38ce4aa9 --- sjsonnet/src/sjsonnet/Evaluator.scala | 1 + sjsonnet/src/sjsonnet/Val.scala | 6 ++++++ sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala | 18 +++++++++++++----- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/sjsonnet/src/sjsonnet/Evaluator.scala b/sjsonnet/src/sjsonnet/Evaluator.scala index 2e52c2cd..4bf8fa50 100644 --- a/sjsonnet/src/sjsonnet/Evaluator.scala +++ b/sjsonnet/src/sjsonnet/Evaluator.scala @@ -857,6 +857,7 @@ class Evaluator( scope: ValScope): Val.Func = new Val.Func(outerPos, scope, params) { override def functionName: String = name + override val bodyExpr: Expr = rhs def evalRhs(vs: ValScope, es: EvalScope, fs: FileScope, pos: Position): Val = visitExprWithTailCallSupport(rhs)(vs) override def evalDefault(expr: Expr, vs: ValScope, es: EvalScope): Val = { diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index ae1bd415..0f44801b 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -770,6 +770,12 @@ object Val { def evalRhs(scope: ValScope, ev: EvalScope, fs: FileScope, pos: Position): Val + /** + * Override to expose the function's body AST for pattern detection (e.g. constant-body + * makeArray optimization). Returns null by default. + */ + def bodyExpr: Expr = null + // Convenience wrapper: evaluates the function body and resolves any TailCall sentinel. // Use this instead of raw `evalRhs` at call sites that bypass `apply*` and consume // the result directly (e.g. stdlib scope-reuse fast paths). diff --git a/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala b/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala index 5c82f8d2..c8908e2d 100644 --- a/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala @@ -469,11 +469,19 @@ object ArrayModule extends AbstractFunctionModule { pos, { val sz = size.cast[Val.Num].asPositiveInt val a = new Array[Eval](sz) - val noOff = pos.noOffset - var i = 0 - while (i < sz) { - a(i) = new LazyApply1(func, Val.Num(pos, i), noOff, ev) - i += 1 + val body = func.bodyExpr + if (func.params.names.length == 1 && body != null && body.isInstanceOf[Val.Literal]) { + // Function body is a constant (e.g. `function(_) 'x'`). + // Skip lazy thunk + Val.Num(index) allocation per element. + val constVal = body.asInstanceOf[Val] + java.util.Arrays.fill(a.asInstanceOf[Array[AnyRef]], constVal) + } else { + val noOff = pos.noOffset + var i = 0 + while (i < sz) { + a(i) = new LazyApply1(func, Val.Num(pos, i), noOff, ev) + i += 1 + } } a }