-
Notifications
You must be signed in to change notification settings - Fork 237
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
Pathological performance with O(n) objects #785
Comments
Object fields are late-bound, thus it is only possible to perform such optimization for object fields, which depend on neither self/super (directly, or through locals), and this requires pretty complicated analysis. As for your code, std.foldl(function (acc, o) acc + { [o.key]: o.val }, arr, {}) It can be replaced with {
[o.key]: o.val
for o in arr
} Which has no such performance problems, and much more readable. |
I've managed to construct a minimal example of this pathology: local ouch(values) =
std.foldl(
function(acc, value)
local lookup = std.get(acc, "a", {});
acc { ["a"]+: lookup },
values,
{}
);
local values = [null for x in std.range(1, 30)];
ouch(values) It takes 80 cpu seconds / 40 wall clock seconds:
CPU profile captured via |
Thinking a bit more about the |
I dug a bit more, preliminary conclusions:
|
Here's a graphviz graph I generated of the resulting object structure. The linked list at the bottom is the initial structure. The fully populated tree at the top is what we end up at the end. What's curious:
|
This optimization works for the minimal reproducer, but unfortunately not for the general case: diff --git a/thunks.go b/thunks.go
index 1cccadb..d677462 100644
--- a/thunks.go
+++ b/thunks.go
@@ -159,6 +159,10 @@ func (f *plusSuperUnboundField) evaluate(i *interpreter, sb selfBinding, origBin
return nil, err
}
+ if left == right {
+ return left, nil
+ }
+
value, err := builtinPlus(i, left, right)
if err != nil {
return nil, err |
We ran into a pathological case of field lookup recently. A common pattern we follow is something along the lines of:
We run into an issue here because internally this effectively creates a linked list:
go-jsonnet/value.go
Lines 618 to 634 in 923f51b
Now, O(1) object lookups become O(n). If we do this in a loop, O(n) can become O(n^2) or worse. We can re-flatten the resulting
extendedObject
into asimpleObject
by using object comprehensions:I'm not sure if there are additional bugs contributing to this behaviour (e.g., why do the caches not absorb this). But ultimately it does raise the question if optimizing for O(n) object merging is the right move. It heavily penalizes reads.
Would it make sense to instead spend more cycles during the object merging? The hacky patch I've attached eliminates the performance issues we faced.
A hybrid approach could also be possible. Perhaps we can establish some sort of depth threshold at which we compact
extendedObject
s down into asimpleObject
.Thanks to @suprememoocow for tracking down the original issue.
The text was updated successfully, but these errors were encountered: