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
2 changes: 1 addition & 1 deletion docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Support for the Protobuf 2 runtime has been removed as of this version. All arti
* **Bug fix** Fix 3 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Bug fix** Fix 4 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Bug fix** Fix topological sort instability [(Issue #2476)](https://github.com/FoundationDB/fdb-record-layer/pull/2476)
* **Performance** Improvement 1 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Performance** Cascades Pre-order Iterable is inefficient [(Issue #2481)](https://github.com/FoundationDB/fdb-record-layer/issues/2481)
* **Performance** Improvement 2 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Performance** Improvement 3 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
* **Performance** Improvement 4 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

import javax.annotation.Nonnull;
import java.util.Objects;
import java.util.stream.StreamSupport;

/**
* This is a simple PoJo hierarchy representing SerDe operations on a predicate comparison of a sparse {@link Index}.
Expand Down Expand Up @@ -85,14 +84,12 @@ public static IndexComparison fromComparison(@Nonnull final Comparisons.Comparis
*/
public static boolean isSupported(@Nonnull final Comparisons.Comparison comparison) {
return comparison instanceof Comparisons.SimpleComparison ||
comparison instanceof Comparisons.NullComparison ||
(comparison instanceof Comparisons.ValueComparison &&
StreamSupport.stream(((Comparisons.ValueComparison)comparison)
.getComparandValue()
.filter(value -> !(value instanceof Value.RangeMatchableValue))
.spliterator(), false)
.findAny()
.isEmpty());
comparison instanceof Comparisons.NullComparison ||
(comparison instanceof Comparisons.ValueComparison &&
((Comparisons.ValueComparison)comparison).getComparandValue().preOrderStream()
.filter(value -> !(value instanceof Value.RangeMatchableValue))
.findAny()
.isEmpty());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
import com.apple.foundationdb.record.query.plan.cascades.values.AbstractValue;
import com.apple.foundationdb.record.query.plan.cascades.values.BooleanValue;
import com.apple.foundationdb.record.query.plan.cascades.values.Value;
import com.apple.foundationdb.record.query.plan.cascades.values.ValueWithChild;
import com.google.auto.service.AutoService;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.protobuf.Message;

import javax.annotation.Nonnull;
Expand All @@ -55,7 +55,7 @@
* A value that flips the output of its boolean child.
*/
@API(API.Status.EXPERIMENTAL)
public class NotValue extends AbstractValue implements BooleanValue {
public class NotValue extends AbstractValue implements BooleanValue, ValueWithChild {
/**
* The hash value of this expression.
*/
Expand Down Expand Up @@ -98,15 +98,20 @@ public Optional<QueryPredicate> toQueryPredicate(@Nullable final TypeRepository

@Nonnull
@Override
public Iterable<? extends Value> getChildren() {
return ImmutableList.of(child);
protected Iterable<? extends Value> computeChildren() {
return ImmutableList.of(getChild());
}

@Nonnull
@Override
public NotValue withChildren(final Iterable<? extends Value> newChildren) {
Verify.verify(Iterables.size(newChildren) == 1);
return new NotValue(Iterables.get(newChildren, 0));
public Value getChild() {
return child;
}

@Nonnull
@Override
public ValueWithChild withNewChild(@Nonnull final Value rebasedChild) {
return new NotValue(rebasedChild);
}

@Nullable
Expand All @@ -124,7 +129,7 @@ public <M extends Message> Object eval(@Nonnull final FDBRecordStoreBase<M> stor
public int hashCodeWithoutChildren() {
return PlanHashable.objectsPlanHash(PlanHashable.CURRENT_FOR_CONTINUATION, BASE_HASH);
}

@Override
public int planHash(@Nonnull final PlanHashMode mode) {
return PlanHashable.objectsPlanHash(mode, BASE_HASH, child);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* PreOrderIterator.java
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2015-2024 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.apple.foundationdb.record.query.plan.cascades;

import com.google.common.base.Verify;
import com.google.common.collect.Iterables;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

/**
* An iterator that accesses all elements of a {@link TreeLike} object in pre-order fashion.
* It attempts to reduce the number of memory allocations while still being performant. It does so
* by implementing a mix of level-based traversal and pre-order traversal, where a {@link Deque} is used
* (as a stack) to enable depth first search, however instead of maintaining individual {@link TreeLike} node in
* each stack frame the entire list of node's children is stored instead in addition to an index pointing out
* to the current element that is returned by the stream.
*
* @param <T> The type of the iterator element.
*/
@NotThreadSafe
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 static final int INITIAL_POSITION = -1;

private PreOrderIterator(@Nonnull final T traversable) {
// initialize the stack with the {@link TreeLike}'s depth as capacity to avoid resizing.
stack = new ArrayDeque<>(traversable.height());
// this is the only list allocation done to put the root in the stack.
// all the remaining lists added to the stack are references to children
// lists (copy by reference).
stack.push(MutablePair.of(List.of(traversable.getThis()), INITIAL_POSITION));
}

@Override
public boolean hasNext() {
while (true) {
if (stack.isEmpty()) {
return false;
}
final var top = Verify.verifyNotNull(stack.peekLast());
final var currentLevelIndex = top.getRight();
final var currentLevelItemsCount = Iterables.size(top.getLeft());
if (currentLevelIndex + 1 < currentLevelItemsCount) {
return true;
} else {
// pop the stack, and continue doing so, until we either find a next element
// by looking into previous levels in reverse (stack) order, progressively
// do so until either an item is found, or the stack becomes empty, and no item
// is to be found meaning that the traversal is complete.
stack.removeLast();
}
}
}

@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException("no more elements");
}
final var top = Verify.verifyNotNull(stack.peekLast());
final var currentIndexPosition = top.getRight();
final var currentLevelItems = top.getLeft();
final var nextItemIndex = currentIndexPosition + 1;

// mark the next item as the current one in this stack frame, note that
// the position must be valid because hasNext() is called first, and hasNext()
// makes sure that, if there is a next element, it modifies the stack index such
// that incrementing it would lead to finding that next element correctly.
final var result = Iterables.get(currentLevelItems, nextItemIndex);
top.setValue(nextItemIndex);
final var resultChildren = result.getChildren();

// descend immediately to the children (if any) so to conform to pre-order DFS semantics.
if (!Iterables.isEmpty(resultChildren)) {
stack.add(MutablePair.of(resultChildren, INITIAL_POSITION));
}
return result;
}

@Nonnull
public static <T extends TreeLike<T>> PreOrderIterator<T> over(@Nonnull final T traversable) {
return new PreOrderIterator<>(traversable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;

/**
* Interface for tree-like structures and helpers for folds and traversals on these tree-like structures of node type
Expand Down Expand Up @@ -69,13 +70,31 @@ public interface TreeLike<T extends TreeLike<T>> {
@Nonnull
T withChildren(Iterable<? extends T> newChildren);

/**
* Returns an iterator that traverse the nodes in pre-order.
* @return an iterator that traverse the nodes in pre-order.
*/
@Nonnull
default Iterator<T> preOrderIterator() {
return PreOrderIterator.over(getThis());
}

/**
* Returns a {@link Stream} that traverses the nodes in pre-order.
* @return a {@link Stream} that traverses the nodes in pre-order.
*/
@Nonnull
default Stream<T> preOrderStream() {
return Streams.stream(preOrderIterator());
}

/**
* Method that returns an {@link Iterable} of nodes as encountered in pre-order traversal of this tree-like.
* @return an {@link Iterable} of nodes
*/
@Nonnull
default Iterable<? extends T> inPreOrder() {
return inPreOrder(self -> true);
default Iterable<? extends T> preOrderIterable() {
return preOrderIterable(self -> true);
}

/**
Expand All @@ -85,13 +104,13 @@ default Iterable<? extends T> inPreOrder() {
* @return an {@link Iterable} of nodes
*/
@Nonnull
default Iterable<? extends T> inPreOrder(@Nonnull final Predicate<T> descentIntoPredicate) {
default Iterable<? extends T> preOrderIterable(@Nonnull final Predicate<T> descentIntoPredicate) {
final ImmutableList.Builder<Iterable<? extends T>> iterablesBuilder = ImmutableList.builder();
final var self = getThis();
iterablesBuilder.add(ImmutableList.of(self));
if (descentIntoPredicate.test(self)) {
for (final T child : getChildren()) {
iterablesBuilder.add(child.inPreOrder());
iterablesBuilder.add(child.preOrderIterable());
}
}
return Iterables.concat(iterablesBuilder.build());
Expand All @@ -102,40 +121,15 @@ default Iterable<? extends T> inPreOrder(@Nonnull final Predicate<T> descentInto
* @return an {@link Iterable} of nodes
*/
@Nonnull
default Iterable<? extends T> inPostOrder() {
default Iterable<? extends T> postOrderIterable() {
final ImmutableList.Builder<Iterable<? extends T>> iterablesBuilder = ImmutableList.builder();
for (final T child : getChildren()) {
iterablesBuilder.add(child.inPostOrder());
iterablesBuilder.add(child.postOrderIterable());
}
iterablesBuilder.add(ImmutableList.of(getThis()));
return Iterables.concat(iterablesBuilder.build());
}

/**
* Method that returns an {@link Iterable} of nodes as encountered in pre-order traversal of this tree-like that
* are filtered by a {@link Predicate} that filters out every node for which {@code predicate} returns {@code false}.
* @param predicate a {@link Predicate} that is evaluated per encountered node during pre-order traversal of
* the tree-like rooted at {@code this}
* @return an {@link Iterable} of nodes satisfying the predicate passed in
*/
@Nonnull
default Iterable<? extends T> filter(@Nonnull final Predicate<T> predicate) {
return Iterables.filter(inPreOrder(), predicate::test);
}

/**
* Method that returns an {@link Iterable} of nodes as encountered in pre-order traversal of this tree-like that
* are filtered by a {@link Predicate} that filters out every node for which {@code predicate} returns {@code false}.
* @param <T1> a fixed sub class of {@code T}
* @param clazz a class object that is used to filter the nodes in this tree-like by their associated class
* @return an {@link Iterable} of nodes that are at least of type {@code T1}
*/
@Nonnull
@SuppressWarnings("unchecked")
default <T1 extends T> Iterable<T1> filter(@Nonnull final Class<T1> clazz) {
return () -> Streams.stream(inPreOrder()).filter(clazz::isInstance).map(t -> (T1)t).iterator();
}

/**
* Method that performs a tree fold over the tree-like rooted at {@code this} using the given map and fold functions.
* Note that this variant of fold does not permit {@code null} values to be produced and consumed by the map and
Expand Down Expand Up @@ -252,6 +246,14 @@ default Optional<T> replaceLeavesMaybe(@Nonnull final UnaryOperator<T> replaceOp
});
}

/**
* returns the height of the tree.
* @return the height of the tree.
*/
default int height() {
return Streams.stream(getChildren()).mapToInt(TreeLike::height).max().orElse(0) + 1;
}

/**
* Functional interface for a function whose result is not nullable.
* @param <T> argument type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.StreamSupport;

/**
* A (relational) expression that has a predicate on it.
Expand Down Expand Up @@ -99,16 +98,11 @@ static ImmutableSet<FieldValue> fieldValuesFromPredicates(@Nonnull final Collect
@Nonnull final Predicate<PredicateWithValue> filteringPredicate) {
return predicates
.stream()
.flatMap(predicate -> {
final Iterable<? extends QueryPredicate> filters =
predicate.filter(p -> p instanceof PredicateWithValue && filteringPredicate.test((PredicateWithValue)p));
return StreamSupport.stream(filters.spliterator(), false)
.map(p -> (PredicateWithValue)p)
.flatMap(predicateWithValue ->
StreamSupport.stream(predicateWithValue.getValue()
.filter(v -> v instanceof FieldValue).spliterator(), false))
.map(value -> (FieldValue)value);
})
.flatMap(predicate -> predicate.preOrderStream()
.filter(p -> p instanceof PredicateWithValue && filteringPredicate.test((PredicateWithValue)p))
.map(p -> (PredicateWithValue)p)
.flatMap(predicateWithValue -> predicateWithValue.getValue().preOrderStream().filter(FieldValue.class::isInstance))
.map(value -> (FieldValue)value))
.map(fieldValue -> {
final Set<CorrelationIdentifier> fieldCorrelatedTo = fieldValue.getChild().getCorrelatedTo();
// TODO make better as the field can currently only handle exactly one correlated alias
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public abstract class AbstractQueryPredicate implements QueryPredicate {

private final Supplier<Integer> semanticHashCodeSupplier;

private final Supplier<Integer> heightSupplier;

@SuppressWarnings("unused")
protected AbstractQueryPredicate(@Nonnull final PlanSerializationContext serializationContext,
@Nonnull final PAbstractQueryPredicate abstractQueryPredicateProto) {
Expand All @@ -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.

}

@Nonnull
Expand Down Expand Up @@ -88,6 +91,11 @@ public int semanticHashCode() {

protected abstract int computeSemanticHashCode();

@Override
public int height() {
return heightSupplier.get();
}

@Override
public int hashCodeWithoutChildren() {
return Objects.hash(isAtomic);
Expand Down
Loading