Skip to content
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

Merged
merged 13 commits into from
Feb 6, 2024

Conversation

hatyo
Copy link
Contributor

@hatyo hatyo commented Jan 24, 2024

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 of Value.
  • 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 a Predicate 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<>.
  • With the new Iterator, the TreeLike now extends Iterable<> interface giving an instance of this iterator when iterator() method is called.
  • Similar to Collection.stream(), a stream() method is added that returns a Stream object which uses the above Iterator implementation.
    I noticed that in almost all callsites, TreeLike#inPreOrder's allocated Iterable is not used, i.e. it is mostly used for stream-like interface filtering, therefore, replacing these calls with the stream() call implemented above is probably more efficient as it does not allocate as many objects. It also streamlines the code better.
  • a height() method is introduced in TreeLike 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, and filter but marked them as deprecated in favour of using stream() (and Stream#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.

@foundationdb-ci

This comment was marked as outdated.

@foundationdb-ci

This comment was marked as outdated.

@foundationdb-ci

This comment was marked as outdated.

@foundationdb-ci

This comment was marked as outdated.

@foundationdb-ci

This comment was marked as outdated.

@hatyo hatyo changed the title WIP - Reduce unnecessary memory allocations. Reduce unnecessary memory allocations in Cascades Jan 25, 2024
Comment on lines 247 to 249
if (obj instanceof PlanHashable) {
return ((PlanHashable)obj).planHash(mode);
}
Copy link
Contributor Author

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:

  1. a Value such as RCV(x,y,z) will match at line 250, calls into iterablePlanHash.
  2. the method iterablePlanHash will iterate over the passed iterable object
  3. the first item in the RCV(x,y,z) is the RCV itself (because of pre-order and the recursive nature of the non-linear structure TreeLike<>.
  4. it calls now in objectPlanHash
  5. it goes back to 1 (!)

-> infinite recursion.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link

sonarcloud bot commented Jan 25, 2024

Quality Gate Passed Quality Gate passed

The SonarCloud Quality Gate passed, but some issues were introduced.

11 New issues
0 Security Hotspots
90.7% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarCloud

@foundationdb-ci
Copy link
Contributor

Result of fdb-record-layer-pr on Linux CentOS 7

  • Commit ID: a1e000b
  • Duration 0:44:20
  • Result: ✅ SUCCEEDED
  • Error: N/A
  • Build Log terminal output (available for 30 days)
  • Build Workspace zip file of the working directory (available for 30 days)

@hatyo hatyo marked this pull request as ready for review January 25, 2024 20:28
Comment on lines 247 to 249
if (obj instanceof PlanHashable) {
return ((PlanHashable)obj).planHash(mode);
}
Copy link
Contributor

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?

*/
@Nonnull
@Deprecated
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@@ -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);
Copy link
Contributor

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

Copy link
Contributor Author

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.

@normen662
Copy link
Contributor

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 replace() changes yet as I hit submit too early. Will do that tomorrow.

@foundationdb-ci
Copy link
Contributor

Result of fdb-record-layer-pr on Linux CentOS 7

  • Commit ID: a1e000b
  • Duration 0:42:11
  • Result: ✅ SUCCEEDED
  • Error: N/A
  • Build Log terminal output (available for 30 days)
  • Build Workspace zip file of the working directory (available for 30 days)

@foundationdb-ci
Copy link
Contributor

Result of fdb-record-layer-pr on Linux CentOS 7

  • Commit ID: 9d65c0a
  • Duration 0:22:45
  • Result: ❌ FAILED
  • Error: Error while executing command: ./gradlew --no-daemon --console=plain -b ./build.gradle build destructiveTest -PcoreNotStrict -PreleaseBuild=false -PpublishBuild=false -PspotbugsEnableHtmlReport. Reason: exit status 1
  • Build Log terminal output (available for 30 days)
  • Build Workspace zip file of the working directory (available for 30 days)

@foundationdb-ci
Copy link
Contributor

Result of fdb-record-layer-pr on Linux CentOS 7

  • Commit ID: b1837e9
  • Duration 0:24:11
  • Result: ❌ FAILED
  • Error: Error while executing command: ./gradlew --no-daemon --console=plain -b ./build.gradle build destructiveTest -PcoreNotStrict -PreleaseBuild=false -PpublishBuild=false -PspotbugsEnableHtmlReport. Reason: exit status 1
  • Build Log terminal output (available for 30 days)
  • Build Workspace zip file of the working directory (available for 30 days)

@foundationdb-ci
Copy link
Contributor

Result of fdb-record-layer-pr on Linux CentOS 7

  • Commit ID: 83aa17f
  • Duration 0:42:10
  • Result: ✅ SUCCEEDED
  • Error: N/A
  • Build Log terminal output (available for 30 days)
  • Build Workspace zip file of the working directory (available for 30 days)

@foundationdb-ci
Copy link
Contributor

Result of fdb-record-layer-pr on Linux CentOS 7

  • Commit ID: 0a0d74f
  • Duration 0:41:03
  • Result: ✅ SUCCEEDED
  • Error: N/A
  • Build Log terminal output (available for 30 days)
  • Build Workspace zip file of the working directory (available for 30 days)

@@ -252,6 +255,8 @@ default Optional<T> replaceLeavesMaybe(@Nonnull final UnaryOperator<T> replaceOp
});
}

int height();
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

@normen662 normen662 Feb 5, 2024

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.

Copy link
Contributor Author

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

}

private int computeHeight() {
return StreamSupport.stream(getChildren().spliterator(), false).mapToInt(TreeLike::height).max().orElse(0) + 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Streams.stream(...) please.

Copy link
Contributor Author

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.

@foundationdb-ci
Copy link
Contributor

Result of fdb-record-layer-pr on Linux CentOS 7

  • Commit ID: 4b62403
  • Duration 0:41:25
  • Result: ✅ SUCCEEDED
  • Error: N/A
  • Build Log terminal output (available for 30 days)
  • Build Workspace zip file of the working directory (available for 30 days)

@foundationdb-ci
Copy link
Contributor

Result of fdb-record-layer-pr on Linux CentOS 7

  • Commit ID: 8ab73b6
  • Duration 0:39:17
  • Result: ✅ SUCCEEDED
  • Error: N/A
  • Build Log terminal output (available for 30 days)
  • Build Workspace zip file of the working directory (available for 30 days)

@@ -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);
Copy link
Contributor

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();
Copy link
Contributor

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.

Copy link
Contributor Author

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

@hatyo hatyo merged commit fc4304a into FoundationDB:main Feb 6, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants