Skip to content

Commit 27832aa

Browse files
authored
perf: improve ConstraintCollectors.toList() (#2285)
The underlying collection is now optimized for removals, as those operations will be frequent inside group nodes. It still retains fast random access to elements, as that is required to reuse it in Neighborhoods. The collection supports null values, and implements the full list interface; this makes it a suitable replacement to ArrayList. Compared to ArrayList, remove() is super-fast if done via the entry. This introduces small overhead in addition and iteration, as the arrays may need to be compacted. Some operations compare equally poorly in both collections, such as additions into the middle of the array.
1 parent 4bb3dd8 commit 27832aa

9 files changed

Lines changed: 1482 additions & 349 deletions

File tree

core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintCollectors.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,7 @@ public final class ConstraintCollectors {
767767
* For stable iteration order, use {@link #toSortedSet()}.
768768
* <p>
769769
* The default result of the collector (e.g. when never called) is an empty {@link Set}.
770+
* The user must not modify this set.
770771
*
771772
* @param <A> type of the matched fact
772773
*/
@@ -779,6 +780,7 @@ public final class ConstraintCollectors {
779780
* {@link ConstraintStream}.
780781
* <p>
781782
* The default result of the collector (e.g. when never called) is an empty {@link SortedSet}.
783+
* The user must not modify this set.
782784
*
783785
* @param <A> type of the matched fact
784786
*/
@@ -800,6 +802,7 @@ public final class ConstraintCollectors {
800802
* For stable iteration order, use {@link #toSortedSet()}.
801803
* <p>
802804
* The default result of the collector (e.g. when never called) is an empty {@link List}.
805+
* The user must not modify this list.
803806
*
804807
* @param <A> type of the matched fact
805808
*/
@@ -976,6 +979,7 @@ public final class ConstraintCollectors {
976979
* For stable iteration order, use {@link #toSortedMap(Function, Function, IntFunction)}.
977980
* <p>
978981
* The default result of the collector (e.g. when never called) is an empty {@link Map}.
982+
* The user must not modify this map.
979983
*
980984
* @param keyMapper map matched fact to a map key
981985
* @param valueMapper map matched fact to a value
@@ -1103,6 +1107,7 @@ public final class ConstraintCollectors {
11031107
* {@code {20: "Ann and Eric", 25: "Beth", 30: "Cathy and David"}}.
11041108
* <p>
11051109
* The default result of the collector (e.g. when never called) is an empty {@link SortedMap}.
1110+
* The user must not modify this map.
11061111
*
11071112
* @param keyMapper map matched fact to a map key
11081113
* @param valueMapper map matched fact to a value

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,9 @@ public final void retractLeft(LeftTuple_ leftTuple) {
119119
return;
120120
}
121121
ListEntry<ExistsCounter<LeftTuple_>> counterEntry = leftTuple.getStore(inputStoreIndexLeftCounterEntry);
122+
var element = counterEntry.element(); // Store so that the reference survives removal.
122123
updateIndexerLeft(compositeKey, counterEntry, leftTuple);
123-
killCounterLeft(counterEntry.element());
124+
killCounterLeft(element);
124125
}
125126

126127
private void updateIndexerLeft(Object compositeKey, ListEntry<ExistsCounter<LeftTuple_>> counterEntry,

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/DefaultUniqueRandomIterator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public boolean hasNext() {
5959
if (nextIndex == -1) {
6060
return false;
6161
}
62-
next = source.get(nextIndex).element();
62+
next = source.get(nextIndex);
6363
indexToOptionallyRemove = -1;
6464
return true;
6565
}

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/RandomAccessIndexerBackend.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ public final class RandomAccessIndexerBackend<T> implements IndexerBackend<T> {
2424

2525
@Override
2626
public ListEntry<T> put(Object compositeKey, T tuple) {
27-
return tupleList.add(tuple);
27+
return tupleList.addEntry(tuple);
2828
}
2929

3030
@Override
3131
public void remove(Object compositeKey, ListEntry<T> entry) {
32-
tupleList.remove((ElementAwareArrayList.Entry<T>) entry);
32+
((ElementAwareArrayList<T>.Entry) entry).remove();
3333
}
3434

3535
@Override

core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/common/AbstractLeftDatasetInstance.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import ai.timefold.solver.core.impl.bavet.common.index.UniqueRandomIterator;
77
import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple;
88
import ai.timefold.solver.core.impl.util.ElementAwareArrayList;
9-
import ai.timefold.solver.core.impl.util.ElementAwareArrayList.Entry;
109

1110
import org.jspecify.annotations.NullMarked;
1211

@@ -35,7 +34,7 @@ public void insert(Tuple_ tuple) {
3534
.formatted(tuple));
3635
}
3736

38-
tuple.setStore(entryStoreIndex, tupleList.add(tuple));
37+
tuple.setStore(entryStoreIndex, tupleList.addEntry(tuple));
3938
}
4039

4140
@Override
@@ -50,13 +49,12 @@ public void update(Tuple_ tuple) {
5049

5150
@Override
5251
public void retract(Tuple_ tuple) {
53-
Entry<Tuple_> entry = tuple.removeStore(entryStoreIndex);
52+
ElementAwareArrayList<Tuple_>.Entry entry = tuple.removeStore(entryStoreIndex);
5453
if (entry == null) {
5554
// No fail fast if null because we don't track which tuples made it through the filter predicate(s)
5655
return;
5756
}
58-
59-
tupleList.remove(entry);
57+
entry.remove();
6058
}
6159

6260
@Override

core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ListUndoableActionable.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
package ai.timefold.solver.core.impl.score.stream.collector;
22

3-
import java.util.ArrayList;
43
import java.util.List;
54

5+
import ai.timefold.solver.core.impl.util.ElementAwareArrayList;
6+
67
public final class ListUndoableActionable<Mapped_> implements UndoableActionable<Mapped_, List<Mapped_>> {
7-
private final List<Mapped_> resultList = new ArrayList<>();
8+
9+
/**
10+
* As long as additions and removals are performed using entry-based methods,
11+
* removals have O(1) performance.
12+
*/
13+
private final ElementAwareArrayList<Mapped_> resultList = new ElementAwareArrayList<>();
814

915
@Override
1016
public Runnable insert(Mapped_ result) {
11-
resultList.add(result);
12-
return () -> resultList.remove(result);
17+
var entry = resultList.addEntry(result);
18+
return entry::remove;
1319
}
1420

1521
@Override

0 commit comments

Comments
 (0)