Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 81 additions & 36 deletions sjsonnet/src/sjsonnet/Evaluator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1197,12 +1197,70 @@ class Evaluator(
newScope
}

// Lazily allocate builder only when we have 2+ fields, avoiding LinkedHashMap for single-field objects
// Lazily allocate builder only when we have more than 8 fields,
// using flat arrays (single-field or inline arrays) for small objects
var builder: java.util.LinkedHashMap[String, Val.Obj.Member] = null
// Track single-field optimization candidates
// Track inline fields: for 1 field use singleKey/singleMember, for 2-8 use arrays
var singleKey: String = null
var singleMember: Val.Obj.Member = null
var inlineKeys: Array[String] = null
var inlineMembers: Array[Val.Obj.Member] = null
var fieldCount = 0
val maxInlineFields = 8

// Shared field-tracking logic: manages singleKey → inlineKeys → builder transitions.
// Handles duplicate key detection at each tier.
def trackField(k: String, v: Val.Obj.Member, offset: Position): Unit = {
if (fieldCount == 0) {
singleKey = k
singleMember = v
} else if (fieldCount == 1) {
// Moving from single-field to multi-field: allocate inline arrays
inlineKeys = new Array[String](math.min(fields.length, maxInlineFields))
inlineMembers = new Array[Val.Obj.Member](inlineKeys.length)
inlineKeys(0) = singleKey
inlineMembers(0) = singleMember
if (singleKey.equals(k)) {
Error.fail(s"Duplicate key $k in evaluated object.", offset)
}
inlineKeys(1) = k
inlineMembers(1) = v
singleKey = null
singleMember = null
} else if (fieldCount <= maxInlineFields && inlineKeys != null) {
// Check for duplicates in inline array
var di = 0
while (di < fieldCount) {
if (inlineKeys(di).equals(k)) {
Error.fail(s"Duplicate key $k in evaluated object.", offset)
}
di += 1
}
if (fieldCount < inlineKeys.length) {
inlineKeys(fieldCount) = k
inlineMembers(fieldCount) = v
} else {
// Overflow: move all inline fields into LinkedHashMap builder
builder = Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](fields.length)
var mi = 0
while (mi < fieldCount) {
builder.put(inlineKeys(mi), inlineMembers(mi))
mi += 1
}
inlineKeys = null
inlineMembers = null
builder.put(k, v)
}
} else {
// Already using builder
val previousValue = builder.put(k, v)
if (previousValue != null) {
Error.fail(s"Duplicate key $k in evaluated object.", offset)
}
}
fieldCount += 1
}

fields.foreach {
case Member.Field(offset, fieldName, plus, null, sep, rhs) =>
val k = visitFieldName(fieldName, offset)
Expand All @@ -1215,23 +1273,7 @@ class Evaluator(
finally decrementStackDepth()
}
}
if (fieldCount == 0) {
singleKey = k
singleMember = v
} else {
if (fieldCount == 1) {
// Moving from single-field to multi-field: allocate builder and add the first field
builder = Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](fields.length)
builder.put(singleKey, singleMember)
singleKey = null
singleMember = null
}
val previousValue = builder.put(k, v)
if (previousValue != null) {
Error.fail(s"Duplicate key $k in evaluated object.", offset)
}
}
fieldCount += 1
trackField(k, v, offset)
}
case Member.Field(offset, fieldName, false, argSpec, sep, rhs) =>
val k = visitFieldName(fieldName, offset)
Expand All @@ -1244,28 +1286,13 @@ class Evaluator(
finally decrementStackDepth()
}
}
if (fieldCount == 0) {
singleKey = k
singleMember = v
} else {
if (fieldCount == 1) {
builder = Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](fields.length)
builder.put(singleKey, singleMember)
singleKey = null
singleMember = null
}
val previousValue = builder.put(k, v)
if (previousValue != null) {
Error.fail(s"Duplicate key $k in evaluated object.", offset)
}
}
fieldCount += 1
trackField(k, v, offset)
}
case _ =>
Error.fail("This case should never be hit", objPos)
}
val valueCache = if (sup == null) {
Val.Obj.getEmptyValueCacheForObjWithoutSuper(fields.length)
Val.Obj.getEmptyValueCacheForObjWithoutSuper(fieldCount)
} else {
new java.util.HashMap[Any, Val]()
}
Expand All @@ -1281,6 +1308,24 @@ class Evaluator(
singleFieldKey = singleKey,
singleFieldMember = singleMember
)
} else if (inlineKeys != null && fieldCount >= 2) {
// Multi-field inline object: use flat arrays instead of LinkedHashMap
val finalKeys =
if (fieldCount == inlineKeys.length) inlineKeys
else java.util.Arrays.copyOf(inlineKeys, fieldCount)
val finalMembers =
if (fieldCount == inlineMembers.length) inlineMembers
else java.util.Arrays.copyOf(inlineMembers, fieldCount)
new Val.Obj(
objPos,
null,
false,
if (asserts != null) triggerAsserts else null,
sup,
valueCache,
inlineFieldKeys = finalKeys,
inlineFieldMembers = finalMembers
)
} else {
new Val.Obj(
objPos,
Expand Down
92 changes: 88 additions & 4 deletions sjsonnet/src/sjsonnet/Val.scala
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,9 @@ object Val {
private var allKeys: util.LinkedHashMap[String, java.lang.Boolean] = null,
private val excludedKeys: java.util.Set[String] = null,
private val singleFieldKey: String = null,
private val singleFieldMember: Obj.Member = null)
private val singleFieldMember: Obj.Member = null,
private val inlineFieldKeys: Array[String] = null,
private val inlineFieldMembers: Array[Obj.Member] = null)
extends Literal
with Expr.ObjBody {
private[sjsonnet] def valTag: Byte = TAG_OBJ
Expand All @@ -474,6 +476,18 @@ object Val {
val m = Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](1)
m.put(singleFieldKey, singleFieldMember)
this.value0 = m
} else if (inlineFieldKeys != null) {
// Multi-field inline object: lazily construct LinkedHashMap from arrays
val keys = inlineFieldKeys
val members = inlineFieldMembers
val n = keys.length
val m = Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](n)
var i = 0
while (i < n) {
m.put(keys(i), members(i))
i += 1
}
this.value0 = m
} else {
// value0 is always defined for non-static objects, so if we're computing it here
// then that implies that the object is static and therefore valueCache should be
Expand Down Expand Up @@ -682,6 +696,7 @@ object Val {

@inline def hasKeys: Boolean = {
if (singleFieldKey != null) true
else if (inlineFieldKeys != null && `super` == null) inlineFieldKeys.length > 0
else {
val m = if (static || `super` != null) getAllKeys else getValue0
!m.isEmpty
Expand All @@ -690,7 +705,16 @@ object Val {

@inline def containsKey(k: String): Boolean = {
if (singleFieldKey != null && `super` == null) singleFieldKey.equals(k)
else {
else if (inlineFieldKeys != null && `super` == null) {
val keys = inlineFieldKeys
val n = keys.length
var i = 0
while (i < n) {
if (keys(i).equals(k)) return true
i += 1
}
false
} else {
val m = if (static || `super` != null) getAllKeys else getValue0
m.containsKey(k)
}
Expand All @@ -699,20 +723,55 @@ object Val {
@inline def containsVisibleKey(k: String): Boolean = {
if (static || `super` != null) {
getAllKeys.get(k) == java.lang.Boolean.FALSE
} else if (inlineFieldKeys != null) {
val keys = inlineFieldKeys
val members = inlineFieldMembers
val n = keys.length
var i = 0
while (i < n) {
if (keys(i).equals(k)) return members(i).visibility != Visibility.Hidden
i += 1
}
false
} else {
val m = getValue0.get(k)
m != null && (m.visibility != Visibility.Hidden)
}
}

lazy val allKeyNames: Array[String] = {
val m = if (static || `super` != null) getAllKeys else getValue0
m.keySet().toArray(new Array[String](m.size()))
if (inlineFieldKeys != null && `super` == null) inlineFieldKeys.clone()
else {
val m = if (static || `super` != null) getAllKeys else getValue0
m.keySet().toArray(new Array[String](m.size()))
}
}

lazy val visibleKeyNames: Array[String] = {
if (static) {
allKeyNames
} else if (inlineFieldKeys != null && `super` == null) {
// Inline multi-field fast path: check if all visible (common case)
val keys = inlineFieldKeys
val members = inlineFieldMembers
val n = keys.length
var allVisible = true
var i = 0
while (allVisible && i < n) {
if (members(i).visibility == Visibility.Hidden) allVisible = false
i += 1
}
if (allVisible) keys.clone()
else {
val buf = new mutable.ArrayBuilder.ofRef[String]
buf.sizeHint(n)
var j = 0
while (j < n) {
if (members(j).visibility != Visibility.Hidden) buf += keys(j)
j += 1
}
buf.result()
}
} else {
val buf = new mutable.ArrayBuilder.ofRef[String]
if (`super` == null) {
Expand Down Expand Up @@ -814,6 +873,31 @@ object Val {
} else {
if (s == null) null else s.valueRaw(k, self, pos, addTo, addKey)
}
} else if (inlineFieldKeys != null) {
// Inline multi-field fast path: linear scan over small arrays
val keys = inlineFieldKeys
val members = inlineFieldMembers
val n = keys.length
var i = 0
while (i < n) {
if (keys(i).equals(k)) {
val m = members(i)
if (!evaluator.settings.brokenAssertionLogic || !m.deprecatedSkipAsserts) {
self.triggerAllAsserts(evaluator.settings.brokenAssertionLogic)
}
val vv = m.invoke(self, s, pos.fileScope, evaluator)
val v = if (s != null && m.add) {
s.valueRaw(k, self, pos, null, null) match {
case null => vv
case supValue => mergeMember(supValue, vv, pos)
}
} else vv
if (addTo != null && m.cached) addTo.put(addKey, v)
return v
}
i += 1
}
if (s == null) null else s.valueRaw(k, self, pos, addTo, addKey)
} else {
getValue0.get(k) match {
case null =>
Expand Down
Loading