Skip to content

Commit 5443347

Browse files
authored
perf: minor improvements to Constraint Streams (#830)
- Iterating manually in join allows to create much less instances of `Iterator`, reducing GC pressure significantly. - min/max constraint collector was needlessly using a `Map`, which only ever had one key. In cases particularly exposed to these issues, the performance improvements seen were ~ 10 %.
1 parent 4f19adb commit 5443347

File tree

3 files changed

+61
-72
lines changed

3 files changed

+61
-72
lines changed

core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/AbstractJoinNode.java

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ protected AbstractJoinNode(int inputStoreIndexLeftOutTupleList, int inputStoreIn
5151
protected abstract boolean testFiltering(LeftTuple_ leftTuple, UniTuple<Right_> rightTuple);
5252

5353
protected final void insertOutTuple(LeftTuple_ leftTuple, UniTuple<Right_> rightTuple) {
54-
OutTuple_ outTuple = createOutTuple(leftTuple, rightTuple);
54+
var outTuple = createOutTuple(leftTuple, rightTuple);
5555
ElementAwareList<OutTuple_> outTupleListLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleList);
56-
ElementAwareListEntry<OutTuple_> outEntryLeft = outTupleListLeft.add(outTuple);
56+
var outEntryLeft = outTupleListLeft.add(outTuple);
5757
outTuple.setStore(outputStoreIndexLeftOutEntry, outEntryLeft);
5858
ElementAwareList<OutTuple_> outTupleListRight = rightTuple.getStore(inputStoreIndexRightOutTupleList);
59-
ElementAwareListEntry<OutTuple_> outEntryRight = outTupleListRight.add(outTuple);
59+
var outEntryRight = outTupleListRight.add(outTuple);
6060
outTuple.setStore(outputStoreIndexRightOutEntry, outEntryRight);
6161
propagationQueue.insert(outTuple);
6262
}
@@ -72,7 +72,7 @@ protected final void innerUpdateLeft(LeftTuple_ leftTuple, Consumer<Consumer<Uni
7272
ElementAwareList<OutTuple_> outTupleListLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleList);
7373
// Propagate the update for downstream filters, matchWeighers, ...
7474
if (!isFiltering) {
75-
for (OutTuple_ outTuple : outTupleListLeft) {
75+
for (var outTuple : outTupleListLeft) {
7676
updateOutTupleLeft(outTuple, leftTuple);
7777
}
7878
} else {
@@ -89,7 +89,7 @@ private void updateOutTupleLeft(OutTuple_ outTuple, LeftTuple_ leftTuple) {
8989
}
9090

9191
private void doUpdateOutTuple(OutTuple_ outTuple) {
92-
TupleState state = outTuple.state;
92+
var state = outTuple.state;
9393
if (!state.isActive()) { // Impossible because they shouldn't linger in the indexes.
9494
throw new IllegalStateException("Impossible state: The tuple (" + outTuple.state + ") in node (" +
9595
this + ") is in an unexpected state (" + outTuple.state + ").");
@@ -104,7 +104,7 @@ protected final void innerUpdateRight(UniTuple<Right_> rightTuple, Consumer<Cons
104104
ElementAwareList<OutTuple_> outTupleListRight = rightTuple.getStore(inputStoreIndexRightOutTupleList);
105105
if (!isFiltering) {
106106
// Propagate the update for downstream filters, matchWeighers, ...
107-
for (OutTuple_ outTuple : outTupleListRight) {
107+
for (var outTuple : outTupleListRight) {
108108
setOutTupleRightFact(outTuple, rightTuple);
109109
doUpdateOutTuple(outTuple);
110110
}
@@ -118,7 +118,7 @@ protected final void innerUpdateRight(UniTuple<Right_> rightTuple, Consumer<Cons
118118

119119
private void processOutTupleUpdate(LeftTuple_ leftTuple, UniTuple<Right_> rightTuple, ElementAwareList<OutTuple_> outList,
120120
ElementAwareList<OutTuple_> outTupleList, int outputStoreIndexOutEntry) {
121-
OutTuple_ outTuple = findOutTuple(outTupleList, outList, outputStoreIndexOutEntry);
121+
var outTuple = findOutTuple(outTupleList, outList, outputStoreIndexOutEntry);
122122
if (testFiltering(leftTuple, rightTuple)) {
123123
if (outTuple == null) {
124124
insertOutTuple(leftTuple, rightTuple);
@@ -132,15 +132,19 @@ private void processOutTupleUpdate(LeftTuple_ leftTuple, UniTuple<Right_> rightT
132132
}
133133
}
134134

135-
private OutTuple_ findOutTuple(ElementAwareList<OutTuple_> outTupleList, ElementAwareList<OutTuple_> outList,
136-
int outputStoreIndexOutEntry) {
135+
private static <Tuple_ extends AbstractTuple> Tuple_ findOutTuple(ElementAwareList<Tuple_> outTupleList,
136+
ElementAwareList<Tuple_> outList, int outputStoreIndexOutEntry) {
137137
// Hack: the outTuple has no left/right input tuple reference, use the left/right outList reference instead.
138-
for (OutTuple_ outTuple : outTupleList) {
139-
ElementAwareListEntry<OutTuple_> outEntry = outTuple.getStore(outputStoreIndexOutEntry);
140-
ElementAwareList<OutTuple_> outEntryList = outEntry.getList();
138+
var item = outTupleList.first();
139+
while (item != null) {
140+
// Creating list iterators here caused major GC pressure; therefore, we iterate over the entries directly.
141+
var outTuple = item.getElement();
142+
ElementAwareListEntry<Tuple_> outEntry = outTuple.getStore(outputStoreIndexOutEntry);
143+
var outEntryList = outEntry.getList();
141144
if (outList == outEntryList) {
142145
return outTuple;
143146
}
147+
item = item.next();
144148
}
145149
return null;
146150
}
@@ -150,7 +154,7 @@ protected final void retractOutTuple(OutTuple_ outTuple) {
150154
outEntryLeft.remove();
151155
ElementAwareListEntry<OutTuple_> outEntryRight = outTuple.removeStore(outputStoreIndexRightOutEntry);
152156
outEntryRight.remove();
153-
TupleState state = outTuple.state;
157+
var state = outTuple.state;
154158
if (!state.isActive()) {
155159
// Impossible because they shouldn't linger in the indexes.
156160
throw new IllegalStateException("Impossible state: The tuple (" + outTuple.state + ") in node (" + this

core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/index/ComparisonIndexer.java

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,20 @@
1212
import ai.timefold.solver.core.impl.util.ElementAwareListEntry;
1313

1414
final class ComparisonIndexer<T, Key_ extends Comparable<Key_>>
15-
implements ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T> {
15+
implements Indexer<T> {
1616

1717
private final int propertyIndex;
18-
private final Supplier<ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T>> downstreamIndexerSupplier;
18+
private final Supplier<Indexer<T>> downstreamIndexerSupplier;
1919
private final Comparator<Key_> keyComparator;
2020
private final boolean hasOrEquals;
21-
private final NavigableMap<Key_, ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T>> comparisonMap;
21+
private final NavigableMap<Key_, Indexer<T>> comparisonMap;
2222

23-
public ComparisonIndexer(JoinerType comparisonJoinerType,
24-
Supplier<ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T>> downstreamIndexerSupplier) {
23+
public ComparisonIndexer(JoinerType comparisonJoinerType, Supplier<Indexer<T>> downstreamIndexerSupplier) {
2524
this(comparisonJoinerType, 0, downstreamIndexerSupplier);
2625
}
2726

2827
public ComparisonIndexer(JoinerType comparisonJoinerType, int propertyIndex,
29-
Supplier<ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T>> downstreamIndexerSupplier) {
28+
Supplier<Indexer<T>> downstreamIndexerSupplier) {
3029
this.propertyIndex = propertyIndex;
3130
this.downstreamIndexerSupplier = Objects.requireNonNull(downstreamIndexerSupplier);
3231
/*
@@ -44,11 +43,10 @@ public ComparisonIndexer(JoinerType comparisonJoinerType, int propertyIndex,
4443
}
4544

4645
@Override
47-
public ElementAwareListEntry<T>
48-
put(ai.timefold.solver.core.impl.score.stream.bavet.common.index.IndexProperties indexProperties, T tuple) {
46+
public ElementAwareListEntry<T> put(IndexProperties indexProperties, T tuple) {
4947
Key_ indexKey = indexProperties.toKey(propertyIndex);
5048
// Avoids computeIfAbsent in order to not create lambdas on the hot path.
51-
ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T> downstreamIndexer = comparisonMap.get(indexKey);
49+
var downstreamIndexer = comparisonMap.get(indexKey);
5250
if (downstreamIndexer == null) {
5351
downstreamIndexer = downstreamIndexerSupplier.get();
5452
comparisonMap.put(indexKey, downstreamIndexer);
@@ -57,22 +55,17 @@ public ComparisonIndexer(JoinerType comparisonJoinerType, int propertyIndex,
5755
}
5856

5957
@Override
60-
public void remove(ai.timefold.solver.core.impl.score.stream.bavet.common.index.IndexProperties indexProperties,
61-
ElementAwareListEntry<T> entry) {
58+
public void remove(IndexProperties indexProperties, ElementAwareListEntry<T> entry) {
6259
Key_ indexKey = indexProperties.toKey(propertyIndex);
63-
ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T> downstreamIndexer =
64-
getDownstreamIndexer(indexProperties, indexKey, entry);
60+
var downstreamIndexer = getDownstreamIndexer(indexProperties, indexKey, entry);
6561
downstreamIndexer.remove(indexProperties, entry);
6662
if (downstreamIndexer.isEmpty()) {
6763
comparisonMap.remove(indexKey);
6864
}
6965
}
7066

71-
private ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T> getDownstreamIndexer(
72-
ai.timefold.solver.core.impl.score.stream.bavet.common.index.IndexProperties indexProperties, Key_ indexerKey,
73-
ElementAwareListEntry<T> entry) {
74-
ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T> downstreamIndexer =
75-
comparisonMap.get(indexerKey);
67+
private Indexer<T> getDownstreamIndexer(IndexProperties indexProperties, Key_ indexerKey, ElementAwareListEntry<T> entry) {
68+
var downstreamIndexer = comparisonMap.get(indexerKey);
7669
if (downstreamIndexer == null) {
7770
throw new IllegalStateException("Impossible state: the tuple (" + entry.getElement()
7871
+ ") with indexProperties (" + indexProperties
@@ -83,16 +76,15 @@ private ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T>
8376

8477
// TODO clean up DRY
8578
@Override
86-
public int size(ai.timefold.solver.core.impl.score.stream.bavet.common.index.IndexProperties indexProperties) {
87-
int mapSize = comparisonMap.size();
79+
public int size(IndexProperties indexProperties) {
80+
var mapSize = comparisonMap.size();
8881
if (mapSize == 0) {
8982
return 0;
9083
}
9184
Key_ indexKey = indexProperties.toKey(propertyIndex);
9285
if (mapSize == 1) { // Avoid creation of the entry set and iterator.
93-
Map.Entry<Key_, ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T>> entry =
94-
comparisonMap.firstEntry();
95-
int comparison = keyComparator.compare(entry.getKey(), indexKey);
86+
var entry = comparisonMap.firstEntry();
87+
var comparison = keyComparator.compare(entry.getKey(), indexKey);
9688
if (comparison >= 0) { // Possibility of reaching the boundary condition.
9789
if (comparison > 0 || !hasOrEquals) {
9890
// Boundary condition reached when we're out of bounds entirely, or when GTE/LTE is not allowed.
@@ -101,10 +93,9 @@ public int size(ai.timefold.solver.core.impl.score.stream.bavet.common.index.Ind
10193
}
10294
return entry.getValue().size(indexProperties);
10395
} else {
104-
int size = 0;
105-
for (Map.Entry<Key_, ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T>> entry : comparisonMap
106-
.entrySet()) {
107-
int comparison = keyComparator.compare(entry.getKey(), indexKey);
96+
var size = 0;
97+
for (var entry : comparisonMap.entrySet()) {
98+
var comparison = keyComparator.compare(entry.getKey(), indexKey);
10899
if (comparison >= 0) { // Possibility of reaching the boundary condition.
109100
if (comparison > 0 || !hasOrEquals) {
110101
// Boundary condition reached when we're out of bounds entirely, or when GTE/LTE is not allowed.
@@ -119,33 +110,29 @@ public int size(ai.timefold.solver.core.impl.score.stream.bavet.common.index.Ind
119110
}
120111

121112
@Override
122-
public void forEach(ai.timefold.solver.core.impl.score.stream.bavet.common.index.IndexProperties indexProperties,
123-
Consumer<T> tupleConsumer) {
124-
int size = comparisonMap.size();
113+
public void forEach(IndexProperties indexProperties, Consumer<T> tupleConsumer) {
114+
var size = comparisonMap.size();
125115
if (size == 0) {
126116
return;
127117
}
128118
Key_ indexKey = indexProperties.toKey(propertyIndex);
129119
if (size == 1) { // Avoid creation of the entry set and iterator.
130-
Map.Entry<Key_, ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T>> entry =
131-
comparisonMap.firstEntry();
120+
var entry = comparisonMap.firstEntry();
132121
visitEntry(indexProperties, tupleConsumer, indexKey, entry);
133122
} else {
134-
for (Map.Entry<Key_, ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T>> entry : comparisonMap
135-
.entrySet()) {
136-
boolean boundaryReached = visitEntry(indexProperties, tupleConsumer, indexKey, entry);
123+
for (var entry : comparisonMap.entrySet()) {
124+
var boundaryReached = visitEntry(indexProperties, tupleConsumer, indexKey, entry);
137125
if (boundaryReached) {
138126
return;
139127
}
140128
}
141129
}
142130
}
143131

144-
private boolean visitEntry(ai.timefold.solver.core.impl.score.stream.bavet.common.index.IndexProperties indexProperties,
145-
Consumer<T> tupleConsumer,
146-
Key_ indexKey, Map.Entry<Key_, ai.timefold.solver.core.impl.score.stream.bavet.common.index.Indexer<T>> entry) {
132+
private boolean visitEntry(IndexProperties indexProperties, Consumer<T> tupleConsumer, Key_ indexKey,
133+
Map.Entry<Key_, Indexer<T>> entry) {
147134
// Comparator matches the order of iteration of the map, so the boundary is always found from the bottom up.
148-
int comparison = keyComparator.compare(entry.getKey(), indexKey);
135+
var comparison = keyComparator.compare(entry.getKey(), indexKey);
149136
if (comparison >= 0) { // Possibility of reaching the boundary condition.
150137
if (comparison > 0 || !hasOrEquals) {
151138
// Boundary condition reached when we're out of bounds entirely, or when GTE/LTE is not allowed.
Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
package ai.timefold.solver.core.impl.score.stream.collector;
22

33
import java.util.Comparator;
4-
import java.util.LinkedHashMap;
5-
import java.util.Map;
64
import java.util.NavigableMap;
75
import java.util.TreeMap;
86
import java.util.function.Function;
97

108
import ai.timefold.solver.core.impl.util.ConstantLambdaUtils;
119
import ai.timefold.solver.core.impl.util.MutableInt;
1210

13-
public final class MinMaxUndoableActionable<Result_, Property_> implements UndoableActionable<Result_, Result_> {
11+
public final class MinMaxUndoableActionable<Result_, Property_>
12+
implements UndoableActionable<Result_, Result_> {
13+
1414
private final boolean isMin;
15-
private final NavigableMap<Property_, Map<Result_, MutableInt>> propertyToItemCountMap;
15+
private final NavigableMap<Property_, ItemCount<Result_>> propertyToItemCountMap;
1616
private final Function<? super Result_, ? extends Property_> propertyFunction;
1717

18-
private MinMaxUndoableActionable(boolean isMin,
19-
NavigableMap<Property_, Map<Result_, MutableInt>> propertyToItemCountMap,
18+
private MinMaxUndoableActionable(boolean isMin, NavigableMap<Property_, ItemCount<Result_>> propertyToItemCountMap,
2019
Function<? super Result_, ? extends Property_> propertyFunction) {
2120
this.isMin = isMin;
2221
this.propertyToItemCountMap = propertyToItemCountMap;
@@ -40,30 +39,29 @@ public static <Result> MinMaxUndoableActionable<Result, Result> maxCalculator(Co
4039
}
4140

4241
public static <Result, Property extends Comparable<? super Property>> MinMaxUndoableActionable<Result, Property>
43-
minCalculator(
44-
Function<? super Result, ? extends Property> propertyMapper) {
42+
minCalculator(Function<? super Result, ? extends Property> propertyMapper) {
4543
return new MinMaxUndoableActionable<>(true, new TreeMap<>(), propertyMapper);
4644
}
4745

4846
public static <Result, Property extends Comparable<? super Property>> MinMaxUndoableActionable<Result, Property>
49-
maxCalculator(
50-
Function<? super Result, ? extends Property> propertyMapper) {
47+
maxCalculator(Function<? super Result, ? extends Property> propertyMapper) {
5148
return new MinMaxUndoableActionable<>(false, new TreeMap<>(), propertyMapper);
5249
}
5350

5451
@Override
5552
public Runnable insert(Result_ item) {
5653
Property_ key = propertyFunction.apply(item);
57-
Map<Result_, MutableInt> itemCountMap = propertyToItemCountMap.computeIfAbsent(key, ignored -> new LinkedHashMap<>());
58-
MutableInt count = itemCountMap.computeIfAbsent(item, ignored -> new MutableInt());
54+
var value = propertyToItemCountMap.get(key);
55+
if (value == null) {
56+
value = new ItemCount<>(item, new MutableInt());
57+
propertyToItemCountMap.put(key, value);
58+
}
59+
var count = value.count;
5960
count.increment();
6061

6162
return () -> {
6263
if (count.decrement() == 0) {
63-
itemCountMap.remove(item);
64-
if (itemCountMap.isEmpty()) {
65-
propertyToItemCountMap.remove(key);
66-
}
64+
propertyToItemCountMap.remove(key);
6765
}
6866
};
6967
}
@@ -73,11 +71,11 @@ public Result_ result() {
7371
if (propertyToItemCountMap.isEmpty()) {
7472
return null;
7573
}
76-
return isMin ? getFirstKey(propertyToItemCountMap.firstEntry().getValue())
77-
: getFirstKey(propertyToItemCountMap.lastEntry().getValue());
74+
var itemCount = isMin ? propertyToItemCountMap.firstEntry().getValue() : propertyToItemCountMap.lastEntry().getValue();
75+
return itemCount.item;
7876
}
7977

80-
private static <Key_> Key_ getFirstKey(Map<Key_, ?> map) {
81-
return map.keySet().iterator().next();
78+
private record ItemCount<Item_>(Item_ item, MutableInt count) {
8279
}
80+
8381
}

0 commit comments

Comments
 (0)