Skip to content

Commit e52f30f

Browse files
authored
perf: remove indirection from ElementAwareArrayList (#2333)
1 parent 7542a7f commit e52f30f

1 file changed

Lines changed: 98 additions & 57 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareArrayList.java

Lines changed: 98 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package ai.timefold.solver.core.impl.util;
22

3+
import java.lang.reflect.Array;
34
import java.util.AbstractList;
4-
import java.util.ArrayList;
5+
import java.util.Arrays;
56
import java.util.ConcurrentModificationException;
67
import java.util.Iterator;
78
import java.util.List;
@@ -24,7 +25,7 @@
2425
* therefore, the insertion position of later elements isn't changed.
2526
* Gaps are removed (the list is fully compacted) when {@link #forEach(Consumer)} or {@link #add(int, Object)} is called.
2627
* {@link #get(int)} and related index-based operations compact only the prefix up to the requested index.
27-
* This keeps the overhead low while giving us most benefits of {@link ArrayList}.
28+
* This keeps the overhead low while giving us most benefits of an array-backed list.
2829
* <p>
2930
* Primary fast-path methods are {@link #addEntry(Object)} and {@link Entry#remove()}, both run in O(1).
3031
* All standard {@link List} methods are also available and may run in O(n) or worse.
@@ -34,11 +35,13 @@
3435
* @param <T>
3536
*/
3637
@NullMarked
37-
public final class ElementAwareArrayList<T extends @Nullable Object> extends AbstractList<T> {
38+
public final class ElementAwareArrayList<T extends @Nullable Object>
39+
extends AbstractList<T> {
3840

3941
private static final int REMOVED_POSITION = -1;
4042

41-
private final List<@Nullable Entry> entryList = new ArrayList<>();
43+
private static final int DEFAULT_CAPACITY = 16;
44+
private @Nullable Entry @Nullable [] entries;
4245
private int lastElementPosition = -1;
4346
private int gapCount = 0; // Always equals the total number of null slots in entryList.
4447

@@ -49,23 +52,41 @@ public final class ElementAwareArrayList<T extends @Nullable Object> extends Abs
4952
*/
5053
public Entry addEntry(T element) {
5154
modCount++;
52-
if (gapCount > 0 && entryList.get(lastElementPosition) == null) { // Reuse a gap if it exists.
55+
if (gapCount > 0 && entries[lastElementPosition] == null) { // Reuse a gap if it exists.
5356
var newEntry = new Entry(element, lastElementPosition);
54-
entryList.set(lastElementPosition, newEntry);
57+
entries[lastElementPosition] = newEntry;
5558
gapCount--;
5659
return newEntry;
5760
}
5861
var newEntry = new Entry(element, ++lastElementPosition);
59-
entryList.add(newEntry);
62+
resize(lastElementPosition + 1);
63+
entries[lastElementPosition] = newEntry;
6064
return newEntry;
6165
}
6266

67+
@SuppressWarnings("unchecked")
68+
private void resize(int minCapacity) {
69+
if (entries == null) {
70+
entries = (Entry[]) Array.newInstance(Entry.class, Math.max(DEFAULT_CAPACITY, minCapacity));
71+
return;
72+
}
73+
if (minCapacity <= entries.length) {
74+
return;
75+
}
76+
entries = Arrays.copyOf(entries, Math.max(entries.length * 2, minCapacity));
77+
}
78+
79+
@Override
80+
public T get(int index) {
81+
return getEntry(index).element();
82+
}
83+
6384
private Entry getEntry(int index) {
6485
if (index < 0 || index >= size()) {
6586
throw new IndexOutOfBoundsException(
6687
"The index (%d) must be >= 0 and < size (%d).".formatted(index, size()));
6788
} else if (gapCount == 0) {
68-
return Objects.requireNonNull(entryList.get(index));
89+
return Objects.requireNonNull(entries[index]);
6990
}
7091
return partialCompact(index);
7192
}
@@ -77,27 +98,24 @@ private Entry partialCompact(int rightBoundaryPosition) {
7798
var encounteredGaps = 0;
7899
var lastNonNullPosition = -1;
79100
for (var currentPosition = 0; currentPosition <= lastElementPosition; currentPosition++) {
80-
var entry = entryList.get(currentPosition);
101+
var entry = entries[currentPosition];
81102
if (entry == null) {
82103
encounteredGaps++;
83104
} else {
84105
lastNonNullPosition++;
85106
if (encounteredGaps > 0) {
86107
var targetPosition = currentPosition - encounteredGaps;
87108
entry.moveTo(targetPosition);
88-
entryList.set(targetPosition, entry);
89-
entryList.set(currentPosition, null); // For consistency; the list is never in an invalid state.
109+
entries[targetPosition] = entry;
110+
entries[currentPosition] = null; // For consistency; the list is never in an invalid state.
90111
modCount++;
91112
}
92113
if (lastNonNullPosition == rightBoundaryPosition) {
93114
// Invariant: positions [0, index] are all non-null,
94115
// so all gapCount nulls lie in [index+1, lastElementPosition].
95116
// If that suffix is entirely nulls (equivalent to index == size()-1), trim it now.
96117
if (gapCount == lastElementPosition - rightBoundaryPosition) {
97-
entryList.subList(rightBoundaryPosition + 1, lastElementPosition + 1).clear();
98-
lastElementPosition = rightBoundaryPosition;
99-
gapCount = 0;
100-
modCount++;
118+
truncateTo(rightBoundaryPosition);
101119
}
102120
return entry;
103121
}
@@ -107,9 +125,15 @@ private Entry partialCompact(int rightBoundaryPosition) {
107125
"The index (%d) must be >= 0 and < size (%d).".formatted(rightBoundaryPosition, size()));
108126
}
109127

110-
@Override
111-
public T get(int index) {
112-
return getEntry(index).element();
128+
private void truncateTo(int newLastPosition) {
129+
if (newLastPosition < 0) {
130+
clear();
131+
return;
132+
}
133+
Arrays.fill(entries, newLastPosition + 1, lastElementPosition + 1, null);
134+
lastElementPosition = newLastPosition;
135+
gapCount = 0;
136+
modCount++;
113137
}
114138

115139
@Override
@@ -131,38 +155,49 @@ public void add(int index, T element) {
131155
return;
132156
}
133157
if (gapCount == 0) {
134-
modCount++;
135-
var newEntry = new Entry(element, index);
136-
entryList.add(index, newEntry);
137-
lastElementPosition++;
138-
for (var i = index + 1; i <= lastElementPosition; i++) {
139-
entryList.get(i).moveTo(i);
140-
}
158+
addWithoutGaps(index, element);
141159
return;
142160
}
143161
// Compact prefix [0, index-1] so physical position k == logical position k for all k < index.
144162
if (index > 0) {
145163
partialCompact(index - 1); // Increases modCount.
146164
}
147-
var newEntry = new Entry(element, index);
148-
if (entryList.get(index) == null) {
165+
if (entries[index] == null) {
149166
// Gap at the target position: fill it directly without shifting the array.
150-
entryList.set(index, newEntry);
167+
entries[index] = new Entry(element, index);
151168
gapCount--;
152169
} else {
153170
// No gap at the target position: rotate entries rightward into the nearest gap in the suffix,
154171
// consuming that gap rather than growing the backing list.
155-
var displaced = newEntry;
156-
for (var i = index; i <= lastElementPosition; i++) {
157-
var current = entryList.get(i);
158-
displaced.moveTo(i);
159-
entryList.set(i, displaced);
160-
if (current == null) {
161-
gapCount--;
162-
break;
163-
}
164-
displaced = current;
172+
addWithGaps(index, new Entry(element, index));
173+
}
174+
}
175+
176+
private void addWithoutGaps(int index, T element) {
177+
modCount++;
178+
var newEntry = new Entry(element, index);
179+
resize(lastElementPosition + 2);
180+
for (var i = lastElementPosition; i >= index; i--) {
181+
var shifted = entries[i];
182+
entries[i + 1] = shifted;
183+
shifted.moveTo(i + 1);
184+
}
185+
entries[index] = newEntry;
186+
lastElementPosition++;
187+
}
188+
189+
private void addWithGaps(int index, Entry newEntry) {
190+
modCount++;
191+
var displaced = newEntry;
192+
for (var i = index; i <= lastElementPosition; i++) {
193+
var current = entries[i];
194+
displaced.moveTo(i);
195+
entries[i] = displaced;
196+
if (current == null) {
197+
gapCount--;
198+
break;
165199
}
200+
displaced = current;
166201
}
167202
}
168203

@@ -191,9 +226,9 @@ private void remove(Entry entry) {
191226
}
192227
var positionPreRemoval = entry.position;
193228
if (positionPreRemoval == lastElementPosition) { // Removing the last element; just trim the list.
194-
entryList.remove(lastElementPosition--);
229+
entries[lastElementPosition--] = null;
195230
} else {
196-
entryList.set(positionPreRemoval, null);
231+
entries[positionPreRemoval] = null;
197232
gapCount++;
198233
}
199234
entry.moveTo(REMOVED_POSITION); // Mark the entry as removed.
@@ -202,11 +237,20 @@ private void remove(Entry entry) {
202237
}
203238

204239
private void clearIfPossible() {
205-
if (gapCount == 0 || lastElementPosition + 1 != gapCount) {
206-
return;
240+
if (isEmpty()) {
241+
// All positions, if any, are gaps. Clear the list entirely.
242+
innerClear();
207243
}
208-
// All positions are gaps. Clear the list entirely.
209-
entryList.clear();
244+
}
245+
246+
@Override
247+
public void clear() {
248+
innerClear();
249+
modCount++;
250+
}
251+
252+
private void innerClear() {
253+
entries = null;
210254
gapCount = 0;
211255
lastElementPosition = -1;
212256
}
@@ -237,7 +281,7 @@ public void forEach(Consumer<? super T> action) {
237281
@SuppressWarnings("DataFlowIssue")
238282
private void forEachWithoutGaps(Consumer<? super T> elementConsumer) {
239283
for (var currentPosition = 0; currentPosition <= lastElementPosition; currentPosition++) {
240-
elementConsumer.accept(entryList.get(currentPosition).element());
284+
elementConsumer.accept(entries[currentPosition].element());
241285
}
242286
}
243287

@@ -252,30 +296,27 @@ private void forEachWithoutGaps(Consumer<? super T> elementConsumer) {
252296
private void forEachCompacting(Consumer<? super T> elementConsumer) {
253297
var liveCount = size();
254298
if (liveCount == 0) {
255-
clearIfPossible(); // The list may still contain gaps, so try to clear it entirely.
299+
clear();
256300
return;
257301
}
258302
var compactPosition = 0;
259303
for (var currentPosition = 0; currentPosition <= lastElementPosition; currentPosition++) {
260-
var entry = entryList.get(currentPosition);
304+
var entry = entries[currentPosition];
261305
if (entry == null) {
262306
continue;
263307
}
264308
elementConsumer.accept(entry.element());
265309
if (currentPosition != compactPosition) {
266310
entry.moveTo(compactPosition);
267-
entryList.set(compactPosition, entry);
268-
entryList.set(currentPosition, null); // Prevent stale data.
311+
entries[compactPosition] = entry;
312+
entries[currentPosition] = null; // Prevent stale data.
269313
modCount++;
270314
}
271315
if (++compactPosition == liveCount) {
272316
break;
273317
}
274318
}
275-
entryList.subList(compactPosition, lastElementPosition + 1).clear();
276-
lastElementPosition = compactPosition - 1;
277-
gapCount = 0;
278-
modCount++;
319+
truncateTo(compactPosition - 1);
279320
}
280321

281322
@Override
@@ -352,9 +393,9 @@ public T next() {
352393
if (logicalPosition >= size()) {
353394
throw new NoSuchElementException();
354395
}
355-
var entry = entryList.get(currentPosition);
396+
var entry = entries[currentPosition];
356397
while (entry == null) {
357-
entry = entryList.get(++currentPosition);
398+
entry = entries[++currentPosition];
358399
}
359400
currentPosition++;
360401
logicalPosition++;
@@ -369,9 +410,9 @@ public T previous() {
369410
if (logicalPosition <= 0) {
370411
throw new NoSuchElementException();
371412
}
372-
var entry = entryList.get(--currentPosition);
413+
Entry entry = null;
373414
while (entry == null) {
374-
entry = entryList.get(--currentPosition);
415+
entry = entries[--currentPosition];
375416
}
376417
logicalPosition--;
377418
lastEntry = entry;

0 commit comments

Comments
 (0)