Skip to content

Commit 92ff8fa

Browse files
committed
perf: reduce allocations in CS
1 parent 3cb5490 commit 92ff8fa

File tree

7 files changed

+137
-55
lines changed

7 files changed

+137
-55
lines changed

core/constraint-streams/src/main/java/ai/timefold/solver/constraint/streams/bavet/common/AbstractIfExistsNode.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,10 @@ protected void decrementCounterRight(ExistsCounter<LeftTuple_> counter) {
123123

124124
protected ElementAwareList<FilteringTracker<LeftTuple_>> updateRightTrackerList(UniTuple<Right_> rightTuple) {
125125
ElementAwareList<FilteringTracker<LeftTuple_>> rightTrackerList = rightTuple.getStore(inputStoreIndexRightTrackerList);
126-
rightTrackerList.forEach(filteringTacker -> {
127-
decrementCounterRight(filteringTacker.counter);
128-
filteringTacker.remove();
129-
});
126+
for (FilteringTracker<LeftTuple_> tuple : rightTrackerList) {
127+
decrementCounterRight(tuple.counter);
128+
tuple.remove();
129+
}
130130
return rightTrackerList;
131131
}
132132

core/constraint-streams/src/main/java/ai/timefold/solver/constraint/streams/bavet/common/AbstractIndexedIfExistsNode.java

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,21 @@ public final void insertLeft(LeftTuple_ leftTuple) {
6262

6363
ExistsCounter<LeftTuple_> counter = new ExistsCounter<>(leftTuple);
6464
ElementAwareListEntry<ExistsCounter<LeftTuple_>> counterEntry = indexerLeft.put(indexProperties, counter);
65-
leftTuple.setStore(inputStoreIndexLeftCounterEntry, counterEntry);
65+
updateCounterRight(leftTuple, indexProperties, counter, counterEntry);
66+
initCounterLeft(counter);
67+
}
6668

69+
private void updateCounterRight(LeftTuple_ leftTuple, IndexProperties indexProperties, ExistsCounter<LeftTuple_> counter,
70+
ElementAwareListEntry<ExistsCounter<LeftTuple_>> counterEntry) {
71+
leftTuple.setStore(inputStoreIndexLeftCounterEntry, counterEntry);
6772
if (!isFiltering) {
6873
counter.countRight = indexerRight.size(indexProperties);
6974
} else {
70-
ElementAwareList<FilteringTracker<LeftTuple_>> leftTrackerList = new ElementAwareList<>();
75+
var leftTrackerList = new ElementAwareList<FilteringTracker<LeftTuple_>>();
7176
indexerRight.forEach(indexProperties,
7277
rightTuple -> updateCounterFromLeft(leftTuple, rightTuple, counter, leftTrackerList));
7378
leftTuple.setStore(inputStoreIndexLeftTrackerList, leftTrackerList);
7479
}
75-
initCounterLeft(counter);
7680
}
7781

7882
@Override
@@ -106,16 +110,7 @@ public final void updateLeft(LeftTuple_ leftTuple) {
106110
updateIndexerLeft(oldIndexProperties, counterEntry, leftTuple);
107111
counter.countRight = 0;
108112
leftTuple.setStore(inputStoreIndexLeftProperties, newIndexProperties);
109-
counterEntry = indexerLeft.put(newIndexProperties, counter);
110-
leftTuple.setStore(inputStoreIndexLeftCounterEntry, counterEntry);
111-
if (!isFiltering) {
112-
counter.countRight = indexerRight.size(newIndexProperties);
113-
} else {
114-
ElementAwareList<FilteringTracker<LeftTuple_>> leftTrackerList = new ElementAwareList<>();
115-
indexerRight.forEach(newIndexProperties,
116-
rightTuple -> updateCounterFromLeft(leftTuple, rightTuple, counter, leftTrackerList));
117-
leftTuple.setStore(inputStoreIndexLeftTrackerList, leftTrackerList);
118-
}
113+
updateCounterRight(leftTuple, newIndexProperties, counter, indexerLeft.put(newIndexProperties, counter));
119114
updateCounterLeft(counter);
120115
}
121116
}
@@ -154,10 +149,14 @@ public final void insertRight(UniTuple<Right_> rightTuple) {
154149

155150
ElementAwareListEntry<UniTuple<Right_>> rightEntry = indexerRight.put(indexProperties, rightTuple);
156151
rightTuple.setStore(inputStoreIndexRightEntry, rightEntry);
152+
updateCounterLeft(rightTuple, indexProperties);
153+
}
154+
155+
private void updateCounterLeft(UniTuple<Right_> rightTuple, IndexProperties indexProperties) {
157156
if (!isFiltering) {
158157
indexerLeft.forEach(indexProperties, this::incrementCounterRight);
159158
} else {
160-
ElementAwareList<FilteringTracker<LeftTuple_>> rightTrackerList = new ElementAwareList<>();
159+
var rightTrackerList = new ElementAwareList<FilteringTracker<LeftTuple_>>();
161160
indexerLeft.forEach(indexProperties, counter -> updateCounterFromRight(rightTuple, counter, rightTrackerList));
162161
rightTuple.setStore(inputStoreIndexRightTrackerList, rightTrackerList);
163162
}
@@ -191,14 +190,7 @@ public final void updateRight(UniTuple<Right_> rightTuple) {
191190
rightTuple.setStore(inputStoreIndexRightProperties, newIndexProperties);
192191
rightEntry = indexerRight.put(newIndexProperties, rightTuple);
193192
rightTuple.setStore(inputStoreIndexRightEntry, rightEntry);
194-
if (!isFiltering) {
195-
indexerLeft.forEach(newIndexProperties, this::incrementCounterRight);
196-
} else {
197-
ElementAwareList<FilteringTracker<LeftTuple_>> rightTrackerList = new ElementAwareList<>();
198-
indexerLeft.forEach(newIndexProperties,
199-
counter -> updateCounterFromRight(rightTuple, counter, rightTrackerList));
200-
rightTuple.setStore(inputStoreIndexRightTrackerList, rightTrackerList);
201-
}
193+
updateCounterLeft(rightTuple, newIndexProperties);
202194
}
203195
}
204196

core/constraint-streams/src/main/java/ai/timefold/solver/constraint/streams/bavet/common/AbstractJoinNode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ private OutTuple_ findOutTuple(ElementAwareList<OutTuple_> outTupleList, Element
143143
ElementAwareListEntry<OutTuple_> outEntry = outTuple.getStore(outputStoreIndexOutEntry);
144144
ElementAwareList<OutTuple_> outEntryList = outEntry.getList();
145145
if (outList == outEntryList) {
146+
outTupleList.prematurelyTerminateIterator(); // Performance optimization.
146147
return outTuple;
147148
}
148149
}

core/constraint-streams/src/main/java/ai/timefold/solver/constraint/streams/bavet/common/AbstractUnindexedIfExistsNode.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ public final void insertLeft(LeftTuple_ leftTuple) {
5252
counter.countRight = rightTupleList.size();
5353
} else {
5454
ElementAwareList<FilteringTracker<LeftTuple_>> leftTrackerList = new ElementAwareList<>();
55-
rightTupleList.forEach(rightTuple -> updateCounterFromLeft(leftTuple, rightTuple, counter, leftTrackerList));
55+
for (UniTuple<Right_> tuple : rightTupleList) {
56+
updateCounterFromLeft(leftTuple, tuple, counter, leftTrackerList);
57+
}
5658
leftTuple.setStore(inputStoreIndexLeftTrackerList, leftTrackerList);
5759
}
5860
initCounterLeft(counter);
@@ -75,7 +77,9 @@ public final void updateLeft(LeftTuple_ leftTuple) {
7577
ElementAwareList<FilteringTracker<LeftTuple_>> leftTrackerList = leftTuple.getStore(inputStoreIndexLeftTrackerList);
7678
leftTrackerList.forEach(FilteringTracker::remove);
7779
counter.countRight = 0;
78-
rightTupleList.forEach(rightTuple -> updateCounterFromLeft(leftTuple, rightTuple, counter, leftTrackerList));
80+
for (UniTuple<Right_> tuple : rightTupleList) {
81+
updateCounterFromLeft(leftTuple, tuple, counter, leftTrackerList);
82+
}
7983
updateCounterLeft(counter);
8084
}
8185
}
@@ -108,7 +112,9 @@ public final void insertRight(UniTuple<Right_> rightTuple) {
108112
leftCounterList.forEach(this::incrementCounterRight);
109113
} else {
110114
ElementAwareList<FilteringTracker<LeftTuple_>> rightTrackerList = new ElementAwareList<>();
111-
leftCounterList.forEach(counter -> updateCounterFromRight(rightTuple, counter, rightTrackerList));
115+
for (ExistsCounter<LeftTuple_> tuple : leftCounterList) {
116+
updateCounterFromRight(rightTuple, tuple, rightTrackerList);
117+
}
112118
rightTuple.setStore(inputStoreIndexRightTrackerList, rightTrackerList);
113119
}
114120
}
@@ -123,7 +129,9 @@ public final void updateRight(UniTuple<Right_> rightTuple) {
123129
}
124130
if (isFiltering) {
125131
ElementAwareList<FilteringTracker<LeftTuple_>> rightTrackerList = updateRightTrackerList(rightTuple);
126-
leftCounterList.forEach(counter -> updateCounterFromRight(rightTuple, counter, rightTrackerList));
132+
for (ExistsCounter<LeftTuple_> tuple : leftCounterList) {
133+
updateCounterFromRight(rightTuple, tuple, rightTrackerList);
134+
}
127135
}
128136
}
129137

core/constraint-streams/src/main/java/ai/timefold/solver/constraint/streams/bavet/common/AbstractUnindexedJoinNode.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ public final void insertLeft(LeftTuple_ leftTuple) {
4444
leftTuple.setStore(inputStoreIndexLeftEntry, leftEntry);
4545
ElementAwareList<OutTuple_> outTupleListLeft = new ElementAwareList<>();
4646
leftTuple.setStore(inputStoreIndexLeftOutTupleList, outTupleListLeft);
47-
rightTupleList.forEach(rightTuple -> insertOutTupleFiltered(leftTuple, rightTuple));
47+
for (UniTuple<Right_> tuple : rightTupleList) {
48+
insertOutTupleFiltered(leftTuple, tuple);
49+
}
4850
}
4951

5052
@Override
@@ -80,7 +82,9 @@ public final void insertRight(UniTuple<Right_> rightTuple) {
8082
rightTuple.setStore(inputStoreIndexRightEntry, rightEntry);
8183
ElementAwareList<OutTuple_> outTupleListRight = new ElementAwareList<>();
8284
rightTuple.setStore(inputStoreIndexRightOutTupleList, outTupleListRight);
83-
leftTupleList.forEach(leftTuple -> insertOutTupleFiltered(leftTuple, rightTuple));
85+
for (LeftTuple_ tuple : leftTupleList) {
86+
insertOutTupleFiltered(tuple, rightTuple);
87+
}
8488
}
8589

8690
@Override

core/constraint-streams/src/main/java/ai/timefold/solver/constraint/streams/bavet/common/index/EqualsIndexer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public int size(IndexProperties indexProperties) {
7070
public void forEach(IndexProperties indexProperties, Consumer<T> tupleConsumer) {
7171
Key_ indexKey = indexProperties.toKey(propertyIndex);
7272
Indexer<T> downstreamIndexer = downstreamIndexerMap.get(indexKey);
73-
if (downstreamIndexer == null || downstreamIndexer.isEmpty()) {
73+
if (downstreamIndexer == null) {
7474
return;
7575
}
7676
downstreamIndexer.forEach(indexProperties, tupleConsumer);

core/core-impl/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareList.java

Lines changed: 99 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,22 @@
77
/**
88
* Linked list that allows to add and remove an element in O(1) time.
99
* Ideal for incremental operations with frequent undo.
10+
* <p>
11+
* This class is not thread-safe.
1012
*
1113
* @param <T> The element type. Often a tuple.
1214
*/
1315
public final class ElementAwareList<T> implements Iterable<T> {
1416

17+
private ElementAwareListIterator sharedIterator;
18+
1519
private int size = 0;
1620
private ElementAwareListEntry<T> first = null;
1721
private ElementAwareListEntry<T> last = null;
1822

23+
public ElementAwareList() {
24+
}
25+
1926
public ElementAwareListEntry<T> add(T tuple) {
2027
ElementAwareListEntry<T> entry = new ElementAwareListEntry<>(this, tuple, last);
2128
if (first == null) {
@@ -56,6 +63,50 @@ public int size() {
5663
return size;
5764
}
5865

66+
/**
67+
* Convenience method for where it is easy to use a non-capturing lambda.
68+
* If a capturing lambda consumer were to be created for this method, use {@link #iterator()} instead,
69+
* which will consume less memory.
70+
* <p>
71+
*
72+
* For example, the following code is perfectly fine:
73+
*
74+
* <code>
75+
* for (int i = 0; i &lt; 3; i++) {
76+
* elementAwareList.forEach(entry -&gt; doSomething(entry));
77+
* }
78+
* </code>
79+
*
80+
* It will create only one lambda instance, regardless of the number of iterations;
81+
* it doesn't need to capture any state.
82+
* On the contrary, the following code will create three instances of a capturing lambda,
83+
* one for each iteration of the for loop:
84+
*
85+
* <code>
86+
* for (int a: List.of(1, 2, 3)) {
87+
* elementAwareList.forEach(entry -&gt; doSomething(entry, a));
88+
* }
89+
* </code>
90+
*
91+
* In this case, the lambda would need to capture "a" which is different in every iteration.
92+
* Therefore, it will generally be better to use the iterator variant,
93+
* as that will only ever create one instance of the iterator,
94+
* regardless of the number of iterations:
95+
*
96+
* <code>
97+
* for (int a: List.of(1, 2, 3)) {
98+
* for (var entry: elementAwareList) {
99+
* doSomething(entry, a);
100+
* }
101+
* }
102+
* </code>
103+
*
104+
* This is only an issue on the hot path,
105+
* where this method can create quite a large garbage collector pressure
106+
* on account of creating throw-away instances of capturing lambdas.
107+
*
108+
* @param tupleConsumer The action to be performed for each element
109+
*/
59110
@Override
60111
public void forEach(Consumer<? super T> tupleConsumer) {
61112
ElementAwareListEntry<T> entry = first;
@@ -67,31 +118,34 @@ public void forEach(Consumer<? super T> tupleConsumer) {
67118
}
68119
}
69120

121+
/**
122+
* See {@link #forEach(Consumer)} for a discussion on the correct use of this method.
123+
*
124+
* @return never null
125+
*/
70126
@Override
71127
public Iterator<T> iterator() {
72-
return new Iterator<>() {
73-
74-
private ElementAwareListEntry<T> nextEntry = first;
75-
76-
@Override
77-
public boolean hasNext() {
78-
if (size == 0) {
79-
return false;
80-
}
81-
return nextEntry != null;
82-
}
83-
84-
@Override
85-
public T next() {
86-
if (!hasNext()) {
87-
throw new NoSuchElementException();
88-
}
89-
T element = nextEntry.getElement();
90-
nextEntry = nextEntry.next;
91-
return element;
92-
}
128+
if (sharedIterator == null || sharedIterator.nextEntry != null) {
129+
// Create a new instance on first access, or when the previous one is still in use (not fully consumed).
130+
sharedIterator = new ElementAwareListIterator();
131+
} else {
132+
// Otherwise the instance is reused, significantly reducing garbage collector pressure when on the hot path.
133+
sharedIterator.nextEntry = first;
134+
}
135+
return sharedIterator;
136+
}
93137

94-
};
138+
/**
139+
* In order for iterator sharing to work properly,
140+
* each iterator must be fully consumed.
141+
* For iterators which are not fully consumed,
142+
* this method can be used to free the iterator to be used by another iteration operation.
143+
* This technically breaks the abstraction of this class,
144+
* but the measured benefit of this change is +5% to 10% of score calculation speed
145+
* on account of not creating gigabytes of iterators per minute.
146+
*/
147+
public void prematurelyTerminateIterator() {
148+
sharedIterator.nextEntry = null;
95149
}
96150

97151
@Override
@@ -114,4 +168,27 @@ public String toString() {
114168
}
115169
}
116170

171+
private final class ElementAwareListIterator implements Iterator<T> {
172+
173+
private ElementAwareListEntry<T> nextEntry = first;
174+
175+
@Override
176+
public boolean hasNext() {
177+
if (size == 0) {
178+
return false;
179+
}
180+
return nextEntry != null;
181+
}
182+
183+
@Override
184+
public T next() {
185+
if (!hasNext()) {
186+
throw new NoSuchElementException();
187+
}
188+
T element = nextEntry.getElement();
189+
nextEntry = nextEntry.next;
190+
return element;
191+
}
192+
193+
}
117194
}

0 commit comments

Comments
 (0)