Skip to content

Commit 268ef50

Browse files
committed
perf: eliminate JVM regression in lazy stdlib init
- Share LazyConstMember objects in StdLibModule companion object so they are reused across Interpreter instances — after first evaluation, cached values are returned directly without closure invocation or Map lookup. - Null out init closure after first invoke to release references for GC. - Replace synchronized lazy val functionMap with unsynchronized var + null-check in AbstractFunctionModule (single-threaded evaluator).
1 parent b48ebfc commit 268ef50

3 files changed

Lines changed: 30 additions & 14 deletions

File tree

sjsonnet/src/sjsonnet/Val.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,12 +639,12 @@ object Val {
639639
* A member whose value is created lazily on first access. Used by stdlib to defer builtin
640640
* function instantiation until actually needed, reducing startup overhead.
641641
*/
642-
final class LazyConstMember(add2: Boolean, visibility2: Visibility, init: () => Val)
642+
final class LazyConstMember(add2: Boolean, visibility2: Visibility, private var init: () => Val)
643643
extends Member(add2, visibility2, cached = true, deprecatedSkipAsserts = true) {
644644
private var _val: Val = _
645645
def invoke(self: Obj, sup: Obj, fs: FileScope, ev: EvalScope): Val = {
646646
var v = _val
647-
if (v eq null) { v = init(); _val = v }
647+
if (v eq null) { v = init(); _val = v; init = null }
648648
v
649649
}
650650
}

sjsonnet/src/sjsonnet/functions/AbstractFunctionModule.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@ abstract class AbstractFunctionModule extends FunctionModule {
1818
def functions: Seq[(String, Val.Func)]
1919

2020
/**
21-
* Lazy function lookup map, built on first access to any function in this module.
21+
* Unsynchronized lazy function lookup map — single-threaded, no synchronization needed.
2222
*/
23-
private lazy val functionMap: Map[String, Val.Func] = functions.toMap
23+
private var _functionMap: Map[String, Val.Func] = _
2424

2525
/**
2626
* Get a function by name, triggering module initialization if needed.
2727
*/
28-
def getFunction(name: String): Val.Func = functionMap(name)
28+
def getFunction(name: String): Val.Func = {
29+
var m = _functionMap
30+
if (m eq null) { m = functions.toMap; _functionMap = m }
31+
m(name)
32+
}
2933

3034
/**
3135
* the holder of the module object

sjsonnet/src/sjsonnet/stdlib/StdLibModule.scala

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,14 @@ final class StdLibModule(
2626

2727
val module: Val.Obj = {
2828
// Estimate total size: module functions + additional std functions + native/trace/extVar + pi/thisFile
29-
val totalSize = nameToModule.size + additionalStdFunctions.size + 3 + additionalStdMembers.size
29+
val totalSize =
30+
sharedLazyMembers.size + additionalStdFunctions.size + 3 + additionalStdMembers.size
3031
val entries = Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](totalSize)
3132

32-
// Lazy members — Val.Builtin created on first access, per-module granularity
33-
val iter = nameToModule.entrySet().iterator()
34-
while (iter.hasNext) {
35-
val e = iter.next()
36-
val n = e.getKey
37-
val m = e.getValue
38-
entries.put(n, new Val.Obj.LazyConstMember(false, Visibility.Hidden, () => m.getFunction(n)))
39-
}
33+
// Shared lazy members — created once in companion object, reused across all instances.
34+
// After first StdLibModule evaluation, LazyConstMember._val is populated and subsequent
35+
// instances get the cached values directly (single null-check fast path).
36+
entries.putAll(sharedLazyMembers)
4037

4138
// Additional std functions (eager — typically empty or small)
4239
for ((k, v) <- additionalStdFunctions)
@@ -85,6 +82,21 @@ object StdLibModule {
8582
m
8683
}
8784

85+
// Shared LazyConstMember objects — created once at class loading, reused across all
86+
// StdLibModule instances. After first evaluation, each member caches its resolved Val.Func,
87+
// so subsequent instances pay only a null-check per member access (no closure or Map lookup).
88+
private val sharedLazyMembers: java.util.LinkedHashMap[String, Val.Obj.Member] = {
89+
val m = new java.util.LinkedHashMap[String, Val.Obj.Member](256)
90+
val iter = nameToModule.entrySet().iterator()
91+
while (iter.hasNext) {
92+
val e = iter.next()
93+
val n = e.getKey
94+
val mod = e.getValue
95+
m.put(n, new Val.Obj.LazyConstMember(false, Visibility.Hidden, () => mod.getFunction(n)))
96+
}
97+
m
98+
}
99+
88100
// Core std library functions that belong directly in StdLibModule
89101
private val traceFunction = new Val.Builtin2("trace", "str", "rest") {
90102
def evalRhs(str: Eval, rest: Eval, ev: EvalScope, pos: Position): Val = {

0 commit comments

Comments
 (0)