-
Notifications
You must be signed in to change notification settings - Fork 104
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
Reduce unnecessary memory allocations in Cascades #2464
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
if (obj instanceof PlanHashable) { | ||
return ((PlanHashable)obj).planHash(mode); | ||
} |
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.
Changing the order here is necessary, because now a Value
is actually an Iterable
, therefore, it will match line 250.
If we do not change the order of lines, the following will happen:
- a
Value
such asRCV(x,y,z)
will match at line 250, calls intoiterablePlanHash
. - the method
iterablePlanHash
will iterate over the passed iterable object - the first item in the
RCV(x,y,z)
is theRCV
itself (because of pre-order and the recursive nature of the non-linear structureTreeLike<>
. - it calls now in
objectPlanHash
- it goes back to 1 (!)
-> infinite recursion.
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.
Is this keeping existing plan hashes stable?
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.
I am assuming the plan hash has enough test coverage such that if the PRB is green (which is), then plan hash should not have been changed.
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.
Restored the code now that TreeLike
does not extend Iterable
.
Quality Gate passedThe SonarCloud Quality Gate passed, but some issues were introduced. 11 New issues |
Result of fdb-record-layer-pr on Linux CentOS 7
|
if (obj instanceof PlanHashable) { | ||
return ((PlanHashable)obj).planHash(mode); | ||
} |
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.
Is this keeping existing plan hashes stable?
...ord-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/TreeLike.java
Outdated
Show resolved
Hide resolved
...ord-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/NotValue.java
Outdated
Show resolved
Hide resolved
...ord-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/NotValue.java
Outdated
Show resolved
Hide resolved
*/ | ||
@Nonnull | ||
@Deprecated |
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.
Please have this use your much better iterator.
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.
Method is replaced with a normal Stream#filter
as now TreeLike
can return a Stream
via TreeLike#preOrderStream()
method.
...re/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/AbstractValue.java
Show resolved
Hide resolved
@@ -68,6 +70,8 @@ public class AndOrValue extends AbstractValue implements BooleanValue { | |||
@Nonnull | |||
private final Value rightChild; | |||
@Nonnull | |||
private final Supplier<Iterable<? extends Value>> childrenSupplier = Suppliers.memoize(this::computeChildren); |
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.
Again, please condense into AbstractValue
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.
Done, I also refactored ValueWithChild
implementations to avoid extra allocation.
Hey, I like the iterator a lot, it's much more performant. Also, quite a bit of resolve to properly profile it. I have not looked into the |
a1e000b
to
9d65c0a
Compare
Result of fdb-record-layer-pr on Linux CentOS 7
|
Result of fdb-record-layer-pr on Linux CentOS 7
|
Result of fdb-record-layer-pr on Linux CentOS 7
|
Result of fdb-record-layer-pr on Linux CentOS 7
|
Result of fdb-record-layer-pr on Linux CentOS 7
|
...ord-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/TreeLike.java
Outdated
Show resolved
Hide resolved
@@ -252,6 +255,8 @@ default Optional<T> replaceLeavesMaybe(@Nonnull final UnaryOperator<T> replaceOp | |||
}); | |||
} | |||
|
|||
int height(); |
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.
Can't we just implement height()
as a static TreeLike
method() that this method calls. That uses a fold()
as there is nothing specific that height()
needs from Value
or QueryPredicate
or such. If you then need to memoize that you can by defining a height() method in the particular AbstractValue
or similar with a memoizing supplier.
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.
Done. Check my reply above.
public final class PreOrderIterator<T extends TreeLike<T>> implements Iterator<T> { | ||
|
||
@Nonnull | ||
private final Deque<Pair<Iterable<? extends T>, Integer>> stack; |
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.
Can you not use Pair
but define your own class? This is due to us removing commons stuff in the future.
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.
Right, but I don't want to do it in this PR to limit its scope, there are 80+ locations where we still use Pair
currently, we can do this in a separate cleanup PR. (I am also using MutablePair
by the way, but we can also replace that in a cleanup PR). #2484
...ord-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/TreeLike.java
Outdated
Show resolved
Hide resolved
...re/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/AbstractValue.java
Show resolved
Hide resolved
} | ||
|
||
private int computeHeight() { | ||
return StreamSupport.stream(getChildren().spliterator(), false).mapToInt(TreeLike::height).max().orElse(0) + 1; |
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.
Streams.stream(...)
please.
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.
Done. Method is refactored and sits now in TreeLike
.
...core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/ExistsValue.java
Outdated
Show resolved
Hide resolved
Result of fdb-record-layer-pr on Linux CentOS 7
|
Result of fdb-record-layer-pr on Linux CentOS 7
|
@@ -56,6 +58,7 @@ protected AbstractQueryPredicate(final boolean isAtomic) { | |||
this.isAtomic = isAtomic; | |||
this.correlatedToSupplier = Suppliers.memoize(this::computeCorrelatedTo); | |||
this.semanticHashCodeSupplier = Suppliers.memoize(this::computeSemanticHashCode); | |||
this.heightSupplier = Suppliers.memoize(QueryPredicate.super::height); |
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.
I like that.
@@ -83,6 +84,12 @@ public Set<CorrelationIdentifier> getCorrelatedToWithoutChildren() { | |||
return Set.of(); |
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.
Can you please make this a ImmutableSet.of()
. You didn't change it here, I just stumbled over it.
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.
ok, I will do as part of solving #2483
This is mainly refactoring to improve memory allocation in Cascades.
Value#getChildren()
is now amortized. Previously, many implementations were calculating the list of children each time the method is invoked, but this is not necessary considering the immutability ofValue
.TreeLike#inPreOrder(Predicate<>)
is slightly incorrect since the predicate does not propagate in recursive call, but more importantly, it is inefficient in terms of memory consumption, considering that it creates a list for each node of the tree each time it is invoked, the lists are accessed via a complex hierarchy of concatenated iterators. These small object allocations are unnecessary and could stress the garbage collection if it is done very frequently which is the case since this method is used indirectly e.g. to understand aPredicate
structure during index matching.In this PR a new implementation that is more efficient is introduced. It employs an algorithm that mixes level-based and preorder-based traversal. It uses a stack which holds a reference to the entire list of children and an index to bookkeep the traversed node. It is exposed as a normal
Iterator<>
.Iterator
, theTreeLike
now extendsIterable<>
interface giving an instance of this iterator wheniterator()
method is called.Collection.stream()
, astream()
method is added that returns aStream
object which uses the aboveIterator
implementation.I noticed that in almost all callsites,
TreeLike#inPreOrder
's allocatedIterable
is not used, i.e. it is mostly used for stream-like interface filtering, therefore, replacing these calls with thestream()
call implemented above is probably more efficient as it does not allocate as many objects. It also streamlines the code better.height()
method is introduced inTreeLike
that returns the max-height of the tree, the implementations of this method are amortized, it is mainly used to set the capacity of the pre-order iterator stack so we can avoid yet other potential reallocations needed for resizing.I kept the
inPreOrder
overloads, andfilter
but marked them as deprecated in favour of usingstream()
(andStream#filter()
). But I am totally fine if we want to remove them immediately.To compare the new implementation of pre-order with the older one, I implemented a simple JMH Benchmark in a separate draft PR #2467, please check it out if you want to get more performance details.