-
Notifications
You must be signed in to change notification settings - Fork 67
Several performance optimizations, primarily aimed at reducing garbage object creation in common cases #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1e35708
5e531ef
9fa504a
a19b92e
3314529
e6ecc8e
21704f2
3ff6f08
017b80c
8054868
bf84312
49b3a52
4cbbe86
6553450
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -381,7 +381,7 @@ class Std(private val additionalNativeFunctions: Map[String, Val.Builtin] = Map. | |
| val func = _func.asFunc | ||
| val obj = _obj.asObj | ||
| val allKeys = obj.allKeyNames | ||
| val m = new util.LinkedHashMap[String, Val.Obj.Member]() | ||
| val m = Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](allKeys.length) | ||
| var i = 0 | ||
| while(i < allKeys.length) { | ||
| val k = allKeys(i) | ||
|
|
@@ -392,7 +392,8 @@ class Std(private val additionalNativeFunctions: Map[String, Val.Builtin] = Map. | |
| m.put(k, v) | ||
| i += 1 | ||
| } | ||
| new Val.Obj(pos, m, false, null, null) | ||
| val valueCache = Val.Obj.getEmptyValueCacheForObjWithoutSuper(allKeys.length) | ||
| new Val.Obj(pos, m, false, null, null, valueCache) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -922,39 +923,110 @@ class Std(private val additionalNativeFunctions: Map[String, Val.Builtin] = Map. | |
| builtin(Range), | ||
| builtin("mergePatch", "target", "patch"){ (pos, ev, target: Val, patch: Val) => | ||
| val mergePosition = pos | ||
| def createMember(v: => Val) = new Val.Obj.Member(false, Visibility.Normal) { | ||
| def createLazyMember(v: => Val) = new Val.Obj.Member(false, Visibility.Normal) { | ||
| def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = v | ||
| } | ||
| def recPair(l: Val, r: Val): Val = (l, r) match{ | ||
| case (l: Val.Obj, r: Val.Obj) => | ||
| val kvs = for { | ||
| k <- (l.visibleKeyNames ++ r.visibleKeyNames).distinct | ||
| lValue = if (l.containsVisibleKey(k)) Option(l.valueRaw(k, l, pos)(ev)) else None | ||
| rValue = if (r.containsVisibleKey(k)) Option(r.valueRaw(k, r, pos)(ev)) else None | ||
| if !rValue.exists(_.isInstanceOf[Val.Null]) | ||
| } yield (lValue, rValue) match{ | ||
| case (Some(lChild), None) => k -> createMember{lChild} | ||
| case (Some(lChild: Val.Obj), Some(rChild: Val.Obj)) => k -> createMember{recPair(lChild, rChild)} | ||
| case (_, Some(rChild)) => k -> createMember{recSingle(rChild)} | ||
| case (None, None) => Error.fail("std.mergePatch: This should never happen") | ||
| val keys: Array[String] = distinctKeys(l.visibleKeyNames, r.visibleKeyNames) | ||
| val kvs: Array[(String, Val.Obj.Member)] = new Array[(String, Val.Obj.Member)](keys.length) | ||
| var kvsIdx = 0 | ||
| var i = 0 | ||
| while (i < keys.length) { | ||
| val key = keys(i) | ||
| val lValue = if (l.containsVisibleKey(key)) l.valueRaw(key, l, pos)(ev) else null | ||
| val rValue = if (r.containsVisibleKey(key)) r.valueRaw(key, r, pos)(ev) else null | ||
| if (!rValue.isInstanceOf[Val.Null]) { // if we are not removing the key | ||
| if (lValue != null && rValue == null) { | ||
| // Preserve the LHS/target value: | ||
| kvs(kvsIdx) = (key, new Val.Obj.ConstMember(false, Visibility.Normal, lValue)) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change to use I'm considering adding a branch in the |
||
| } else if (lValue.isInstanceOf[Val.Obj] && rValue.isInstanceOf[Val.Obj]) { | ||
| // Recursively merge objects: | ||
| kvs(kvsIdx) = (key, createLazyMember(recPair(lValue, rValue))) | ||
| } else if (rValue != null) { | ||
| // Use the RHS/patch value and recursively remove Null or hidden fields: | ||
| kvs(kvsIdx) = (key, createLazyMember(recSingle(rValue))) | ||
| } else { | ||
| Error.fail("std.mergePatch: This should never happen") | ||
| } | ||
| kvsIdx += 1 | ||
| } | ||
| i += 1 | ||
| } | ||
|
|
||
| Val.Obj.mk(mergePosition, kvs:_*) | ||
| val trimmedKvs = if (kvsIdx == i) kvs else kvs.slice(0, kvsIdx) | ||
| Val.Obj.mk(mergePosition, trimmedKvs) | ||
|
|
||
| case (_, _) => recSingle(r) | ||
| } | ||
| def recSingle(v: Val): Val = v match{ | ||
| case obj: Val.Obj => | ||
| val kvs = for{ | ||
| k <- obj.visibleKeyNames | ||
| value = obj.value(k, pos, obj)(ev) | ||
| if !value.isInstanceOf[Val.Null] | ||
| } yield (k, createMember{recSingle(value)}) | ||
|
|
||
| Val.Obj.mk(obj.pos, kvs:_*) | ||
| val keys: Array[String] = obj.visibleKeyNames | ||
| val kvs: Array[(String, Val.Obj.Member)] = new Array[(String, Val.Obj.Member)](keys.length) | ||
| var kvsIdx = 0 | ||
| var i = 0 | ||
| while (i < keys.length) { | ||
| val key = keys(i) | ||
| val value = obj.value(key, pos, obj)(ev) | ||
| if (!value.isInstanceOf[Val.Null]) { | ||
| kvs(kvsIdx) = (key, createLazyMember(recSingle(value))) | ||
| kvsIdx += 1 | ||
| } | ||
| i += 1 | ||
| } | ||
| val trimmedKvs = if (kvsIdx == i) kvs else kvs.slice(0, kvsIdx) | ||
| Val.Obj.mk(obj.pos, trimmedKvs) | ||
|
|
||
| case _ => v | ||
| } | ||
| def distinctKeys(lKeys: Array[String], rKeys: Array[String]): Array[String] = { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I plan to add code comments here and throughout the PR before it merges, BTW. I'll revisit this with fresh eyes tomorrow. |
||
| // Fast path for small RHS size (the common case when merging a small | ||
| // patch into a large target object), avoiding the cost of constructing | ||
| // and probing a hash set: instead, perform a nested loop where the LHS | ||
| // is scanned and matching RHS entries are marked as null to be skipped. | ||
| // Via local microbenchmarks simulating a "worst-case" (RHS keys all new), | ||
| // the threshold of `8` was empirically determined to be a good tradeoff | ||
| // between allocation + hashing costs vs. nested loop array scans. | ||
| if (rKeys.length <= 8) { | ||
| val rKeysCopy = new Array[String](rKeys.length) | ||
| rKeys.copyToArray(rKeysCopy) | ||
| var i = 0 | ||
| var numNewRKeys = rKeysCopy.length | ||
| while (i < lKeys.length) { | ||
| val lKey = lKeys(i) | ||
| var j = 0 | ||
| while (j < rKeysCopy.length) { | ||
| // This LHS key is in the RHS, so mark it to be skipped in output: | ||
| if (lKey == rKeysCopy(j)) { | ||
| rKeysCopy(j) = null | ||
| numNewRKeys -= 1 | ||
| } | ||
| j += 1 | ||
| } | ||
| i += 1 | ||
| } | ||
| // Combine lKeys with non-null elements of rKeysCopy: | ||
| if (numNewRKeys == 0) { | ||
| lKeys | ||
| } else { | ||
| val outArray = new Array[String](lKeys.length + numNewRKeys) | ||
| System.arraycopy(lKeys, 0, outArray, 0, lKeys.length) | ||
| var outIdx = lKeys.length | ||
| var j = 0 | ||
| while (j < rKeysCopy.length) { | ||
| if (rKeysCopy(j) != null) { | ||
| outArray(outIdx) = rKeysCopy(j) | ||
| outIdx += 1 | ||
| } | ||
| j += 1 | ||
| } | ||
| outArray | ||
| } | ||
| } else { | ||
| // Fallback: Use hash-based deduplication for large RHS arrays: | ||
| (lKeys ++ rKeys).distinct | ||
| } | ||
| } | ||
| recPair(target, patch) | ||
| }, | ||
| builtin("sqrt", "x"){ (pos, ev, x: Double) => | ||
|
|
@@ -1417,12 +1489,12 @@ class Std(private val additionalNativeFunctions: Map[String, Val.Builtin] = Map. | |
| } | ||
| def rec(x: Val): Val = x match{ | ||
| case o: Val.Obj => | ||
| val bindings = for{ | ||
| val bindings: Array[(String, Val.Obj.Member)] = for{ | ||
| k <- o.visibleKeyNames | ||
| v = rec(o.value(k, pos.fileScope.noOffsetPos)(ev)) | ||
| if filter(v) | ||
| }yield (k, new Val.Obj.ConstMember(false, Visibility.Normal, v)) | ||
| Val.Obj.mk(pos, bindings: _*) | ||
| Val.Obj.mk(pos, bindings) | ||
| case a: Val.Arr => | ||
| new Val.Arr(pos, a.asStrictArray.map(rec).filter(filter).map(identity)) | ||
| case _ => x | ||
|
|
@@ -1513,12 +1585,12 @@ class Std(private val additionalNativeFunctions: Map[String, Val.Builtin] = Map. | |
| ))) | ||
| }, | ||
| builtin("objectRemoveKey", "obj", "key") { (pos, ev, o: Val.Obj, key: String) => | ||
| val bindings = for{ | ||
| val bindings: Array[(String, Val.Obj.Member)] = for{ | ||
| k <- o.visibleKeyNames | ||
| v = o.value(k, pos.fileScope.noOffsetPos)(ev) | ||
| if k != key | ||
| }yield (k, new Val.Obj.ConstMember(false, Visibility.Normal, v)) | ||
| Val.Obj.mk(pos, bindings: _*) | ||
| Val.Obj.mk(pos, bindings) | ||
| }, | ||
| builtin(MinArray), | ||
| builtin(MaxArray), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better with a
sizeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the PR description and the Scaladoc of
getEmptyValueCacheForObjWithoutSuperfor more details, but this is deliberate:Because this object has a super object, computing an upper bound on the field count is not free. In this PR I'm aiming to be conservative and only down-size in cases where we have a cheap tight bound.
Also, an object might have a large number of fields but many of them might not end up being computed in a particular evaluation or materialization. In those cases, we want to avoid sizing the map larger than the previous default.