Skip to content

Commit b35cde7

Browse files
perf: replace anonymous Member classes with concrete ExprFieldMember/MethodFieldMember (#756)
## Summary - Replace the two anonymous `Val.Obj.Member` subclasses in `visitMemberList` with concrete `ExprFieldMember` and `MethodFieldMember` classes, giving the JIT a monomorphic type at every `invoke()` call site for better inlining. - Extract the shared scope-caching logic (`makeNewScope`/`createNewScope` closures) into an `ObjectScopeFactory` class. All fields from the same object literal share one factory, preserving per-object scope caching while eliminating per-field closure allocations. ## Benchmark results JMH regression suite (`bench.runRegressions`), JDK 21.0.10, single fork, 1 warmup + 1 measurement iteration (10s): | Benchmark | Before | After | Change | |-----------|--------|-------|--------| | bench.07 (object fibonacci) | 9.948 ms | 5.049 ms | **-49.2%** | | comparison | 6.941 ms | 4.201 ms | **-39.5%** | | large_string_template | 5.591 ms | 3.830 ms | **-31.5%** | | base64 | 1.150 ms | 0.812 ms | **-29.4%** | | large_string_join | 1.476 ms | 1.053 ms | **-28.7%** | | base64_byte_array | 2.925 ms | 2.160 ms | **-26.2%** | | bench.04 | 0.486 ms | 0.372 ms | **-23.5%** | | realistic1 | 4.592 ms | 3.550 ms | **-22.7%** | | base64DecodeBytes | 17.514 ms | 13.667 ms | **-22.0%** | | foldl | 0.277 ms | 0.219 ms | **-20.9%** | | gen_big_object | 2.345 ms | 1.947 ms | **-17.0%** | | realistic2 | 115.444 ms | 97.620 ms | **-15.4%** | | assertions | 0.532 ms | 0.464 ms | **-12.8%** | | bench.09 | 0.079 ms | 0.070 ms | **-11.4%** | No regressions observed (remaining benchmarks within noise). ## Test plan - [x] `./mill 'sjsonnet.jvm[3.3.7]'.test` — all tests pass - [x] `./mill __.checkFormat` — formatting verified - [x] JMH regression benchmarks compared to baseline
1 parent c797488 commit b35cde7

1 file changed

Lines changed: 97 additions & 62 deletions

File tree

sjsonnet/src/sjsonnet/Evaluator.scala

Lines changed: 97 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,25 +1612,12 @@ class Evaluator(
16121612
scope: ValScope): Val.Obj = {
16131613
val asserts = e.asserts
16141614
val fields = e.fields
1615-
var cachedSimpleScope: Option[ValScope] = None
1616-
var cachedObj: Val.Obj = null
1617-
1618-
def makeNewScope(self: Val.Obj, sup: Val.Obj): ValScope = {
1619-
if ((sup eq null) && (self eq cachedObj)) {
1620-
cachedSimpleScope match {
1621-
case Some(s) => s
1622-
case None =>
1623-
val newScope = createNewScope(self, sup)
1624-
cachedSimpleScope = Some(newScope)
1625-
newScope
1626-
}
1627-
} else createNewScope(self, sup)
1628-
}
1615+
val factory = new ObjectScopeFactory(scope, e.binds, this)
16291616

16301617
// Trigger an object's own assertions. This defines a closure which is
16311618
// invoked from within Val.Obj; it should not be called directly.
16321619
def triggerAsserts(self: Val.Obj, sup: Val.Obj): Unit = {
1633-
val newScope: ValScope = makeNewScope(self, sup)
1620+
val newScope: ValScope = factory.makeScope(self, sup)
16341621
var i = 0
16351622
while (i < asserts.length) {
16361623
val a = asserts(i)
@@ -1648,33 +1635,6 @@ class Evaluator(
16481635
}
16491636
}
16501637

1651-
def createNewScope(self: Val.Obj, sup: Val.Obj): ValScope = {
1652-
val scopeLen = scope.length
1653-
val binds = e.binds
1654-
val by = if (binds == null) 2 else 2 + binds.length
1655-
val newScope = scope.extendBy(by)
1656-
newScope.bindings(scopeLen) = self
1657-
newScope.bindings(scopeLen + 1) = sup
1658-
if (binds != null) {
1659-
val arrF = newScope.bindings
1660-
var i = 0
1661-
var j = scopeLen + 2
1662-
while (i < binds.length) {
1663-
val b = binds(i)
1664-
arrF(j) = b.args match {
1665-
case null =>
1666-
visitAsLazy(b.rhs)(newScope)
1667-
case argSpec =>
1668-
if (debugStats != null) debugStats.lazyCreated += 1
1669-
new LazyFunc(() => visitMethod(b.rhs, argSpec, b.pos)(newScope))
1670-
}
1671-
i += 1
1672-
j += 1
1673-
}
1674-
}
1675-
newScope
1676-
}
1677-
16781638
// Lazily allocate builder only when we have more than 8 fields,
16791639
// using flat arrays (single-field or inline arrays) for small objects
16801640
var builder: java.util.LinkedHashMap[String, Val.Obj.Member] = null
@@ -1743,28 +1703,12 @@ class Evaluator(
17431703
case Member.Field(offset, fieldName, plus, null, sep, rhs) =>
17441704
val k = visitFieldName(fieldName, offset)
17451705
if (k != null) {
1746-
val fieldKey = k
1747-
val v = new Val.Obj.Member(plus, sep) {
1748-
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = {
1749-
checkStackDepth(rhs.pos, fieldKey)
1750-
try visitExpr(rhs)(makeNewScope(self, sup))
1751-
finally decrementStackDepth()
1752-
}
1753-
}
1754-
trackField(k, v, offset)
1706+
trackField(k, new ExprFieldMember(plus, sep, rhs, k, factory), offset)
17551707
}
17561708
case Member.Field(offset, fieldName, false, argSpec, sep, rhs) =>
17571709
val k = visitFieldName(fieldName, offset)
17581710
if (k != null) {
1759-
val fieldKey = k
1760-
val v = new Val.Obj.Member(false, sep) {
1761-
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = {
1762-
checkStackDepth(rhs.pos, fieldKey)
1763-
try visitMethod(rhs, argSpec, offset)(makeNewScope(self, sup))
1764-
finally decrementStackDepth()
1765-
}
1766-
}
1767-
trackField(k, v, offset)
1711+
trackField(k, new MethodFieldMember(sep, rhs, argSpec, offset, k, factory), offset)
17681712
}
17691713
case _ =>
17701714
Error.fail("This case should never be hit", objPos)
@@ -1774,7 +1718,7 @@ class Evaluator(
17741718
// HashMap allocation overhead for objects with >2 fields.
17751719
val noSelfRef = sup == null && Materializer.computeNoSelfRef(e)
17761720
if (debugStats != null) debugStats.objectsCreated += 1
1777-
cachedObj = if (fieldCount == 1 && singleKey != null) {
1721+
factory.cachedObj = if (fieldCount == 1 && singleKey != null) {
17781722
// Single-field object: store key and member inline, avoid LinkedHashMap allocation entirely
17791723
val obj = new Val.Obj(
17801724
objPos,
@@ -1823,7 +1767,7 @@ class Evaluator(
18231767
sup
18241768
)
18251769
}
1826-
cachedObj
1770+
factory.cachedObj
18271771
}
18281772

18291773
def visitObjComp(e: ObjBody.ObjComp, sup: Val.Obj)(implicit scope: ValScope): Val.Obj = {
@@ -2100,3 +2044,94 @@ object Evaluator {
21002044
val emptyStringArray = new Array[String](0)
21012045
val emptyLazyArray = new Array[Eval](0)
21022046
}
2047+
2048+
/**
2049+
* Shared scope factory for object field evaluation. Encapsulates the scope-caching logic that was
2050+
* previously embedded in `visitMemberList`'s `makeNewScope` closure. All [[ExprFieldMember]] and
2051+
* [[MethodFieldMember]] instances from the same object literal share one factory, preserving the
2052+
* per-object scope cache.
2053+
*/
2054+
private[sjsonnet] final class ObjectScopeFactory(
2055+
private val enclosingScope: ValScope,
2056+
private val binds: Array[Expr.Bind],
2057+
private[sjsonnet] val evaluator: Evaluator) {
2058+
private var cachedScope: ValScope = ValScope.empty
2059+
private var cachedScopeSet: Boolean = false
2060+
private[sjsonnet] var cachedObj: Val.Obj = _
2061+
2062+
def makeScope(self: Val.Obj, sup: Val.Obj): ValScope = {
2063+
if ((sup eq null) && (self eq cachedObj)) {
2064+
if (cachedScopeSet) cachedScope
2065+
else {
2066+
val s = createScope(self, sup)
2067+
cachedScope = s
2068+
cachedScopeSet = true
2069+
s
2070+
}
2071+
} else createScope(self, sup)
2072+
}
2073+
2074+
private def createScope(self: Val.Obj, sup: Val.Obj): ValScope = {
2075+
val scopeLen = enclosingScope.length
2076+
val by = if (binds == null) 2 else 2 + binds.length
2077+
val newScope = enclosingScope.extendBy(by)
2078+
newScope.bindings(scopeLen) = self
2079+
newScope.bindings(scopeLen + 1) = sup
2080+
if (binds != null) {
2081+
val arrF = newScope.bindings
2082+
var i = 0
2083+
var j = scopeLen + 2
2084+
while (i < binds.length) {
2085+
val b = binds(i)
2086+
arrF(j) = b.args match {
2087+
case null =>
2088+
evaluator.visitAsLazy(b.rhs)(newScope)
2089+
case argSpec =>
2090+
if (evaluator.debugStats != null) evaluator.debugStats.lazyCreated += 1
2091+
new LazyFunc(() => evaluator.visitMethod(b.rhs, argSpec, b.pos)(newScope))
2092+
}
2093+
i += 1
2094+
j += 1
2095+
}
2096+
}
2097+
newScope
2098+
}
2099+
}
2100+
2101+
/**
2102+
* Concrete [[Val.Obj.Member]] for expression-body fields (no argSpec). Replaces the anonymous inner
2103+
* class in `visitMemberList`, giving the JIT a single monomorphic type at `invoke` call sites for
2104+
* better inlining.
2105+
*/
2106+
private[sjsonnet] final class ExprFieldMember(
2107+
add0: Boolean,
2108+
vis0: Visibility,
2109+
private val rhs: Expr,
2110+
private val fieldKey: String,
2111+
private val factory: ObjectScopeFactory)
2112+
extends Val.Obj.Member(add0, vis0) {
2113+
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = {
2114+
factory.evaluator.checkStackDepth(rhs.pos, fieldKey)
2115+
try factory.evaluator.visitExpr(rhs)(factory.makeScope(self, sup))
2116+
finally factory.evaluator.decrementStackDepth()
2117+
}
2118+
}
2119+
2120+
/**
2121+
* Concrete [[Val.Obj.Member]] for method-body fields (has argSpec). Same monomorphic-dispatch
2122+
* benefit as [[ExprFieldMember]].
2123+
*/
2124+
private[sjsonnet] final class MethodFieldMember(
2125+
vis0: Visibility,
2126+
private val rhs: Expr,
2127+
private val argSpec: Expr.Params,
2128+
private val methodPos: Position,
2129+
private val fieldKey: String,
2130+
private val factory: ObjectScopeFactory)
2131+
extends Val.Obj.Member(false, vis0) {
2132+
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = {
2133+
factory.evaluator.checkStackDepth(rhs.pos, fieldKey)
2134+
try factory.evaluator.visitMethod(rhs, argSpec, methodPos)(factory.makeScope(self, sup))
2135+
finally factory.evaluator.decrementStackDepth()
2136+
}
2137+
}

0 commit comments

Comments
 (0)