Skip to content

Commit cb59040

Browse files
committed
perf: DynamicIntArray gets different clearing strategies
1 parent 45e6746 commit cb59040

3 files changed

Lines changed: 54 additions & 86 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/LoopedTracker.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package ai.timefold.solver.core.impl.domain.variable.declarative;
22

3+
import static ai.timefold.solver.core.impl.util.DynamicIntArray.ClearingStrategy.PARTIAL;
4+
35
import ai.timefold.solver.core.impl.util.DynamicIntArray;
46

57
import org.jspecify.annotations.NullMarked;
@@ -14,7 +16,8 @@ public final class LoopedTracker {
1416
private final DynamicIntArray looped;
1517

1618
public LoopedTracker(int nodeCount) {
17-
this.looped = new DynamicIntArray(nodeCount);
19+
// We never fully clear the array, as that was shown to cause too much GC pressure.
20+
this.looped = new DynamicIntArray(nodeCount, PARTIAL);
1821
}
1922

2023
public void mark(int node, LoopedStatus status) {

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

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

3+
import java.util.Arrays;
4+
35
/**
46
* A class representing an int array that is dynamically allocated based on the first set index.
57
* The array is only created when the first element is set and is reallocated as needed
@@ -12,6 +14,7 @@ public final class DynamicIntArray {
1214
// Minimum capacity increment to avoid small incremental growth
1315
private static final int MIN_CAPACITY_INCREMENT = 10;
1416

17+
private final ClearingStrategy clearingStrategy;
1518
private final int maxLength;
1619
private int[] array;
1720
private int firstIndex;
@@ -21,12 +24,18 @@ public DynamicIntArray() {
2124
this(Integer.MAX_VALUE);
2225
}
2326

24-
/**
25-
* Creates a new empty DynamicIntArray.
26-
*/
27+
public DynamicIntArray(ClearingStrategy clearingStrategy) {
28+
this(Integer.MAX_VALUE, clearingStrategy);
29+
}
30+
2731
public DynamicIntArray(int maxLength) {
32+
this(maxLength, ClearingStrategy.FULL);
33+
}
34+
35+
public DynamicIntArray(int maxLength, ClearingStrategy clearingStrategy) {
2836
this.maxLength = maxLength;
29-
clear();
37+
this.clearingStrategy = clearingStrategy;
38+
initializeArray();
3039
}
3140

3241
/**
@@ -171,28 +180,47 @@ int getLastIndex() {
171180
return lastIndex;
172181
}
173182

174-
/**
175-
* Gets the length of the array.
176-
*
177-
* @return the length of the array, or 0 if the array is empty
178-
*/
179-
int length() {
180-
if (array == null) {
181-
return 0;
182-
}
183-
return lastIndex + 1;
184-
}
185-
186183
/**
187184
* Clears the array by setting all values to 0.
188185
* The array structure is preserved, only the values are reset.
189186
*/
190187
public void clear() {
191-
// Array is null until first element is set
188+
if (clearingStrategy == ClearingStrategy.FULL) {
189+
initializeArray();
190+
} else {
191+
// If array is null, there's nothing to clear
192+
if (array == null) {
193+
return;
194+
}
195+
196+
// Only clear the used portion of the array (from firstIndex to lastIndex)
197+
// This is more efficient for large arrays with sparse indices
198+
Arrays.fill(array, 0, lastIndex - firstIndex + 1, 0);
199+
}
200+
}
201+
202+
private void initializeArray() {
192203
this.array = null;
193-
// Initialize with invalid indices
194204
this.firstIndex = Integer.MAX_VALUE;
195205
this.lastIndex = Integer.MIN_VALUE;
196206
}
197207

208+
public enum ClearingStrategy {
209+
210+
/**
211+
* The GC will be allowed to reclaim the array.
212+
* This means that, on next access, the array will have to be reallocated and gradually resized,
213+
* possibly leading to excessive GC pressure.
214+
*
215+
* This is the default.
216+
*/
217+
FULL,
218+
/**
219+
* The array will not be returned to GC and will be filled with zeros instead.
220+
* This has no impact on GC, but may result in greater at-rest heap usage than strictly necessary.
221+
*/
222+
PARTIAL
223+
224+
}
225+
198226
}

core/src/test/java/ai/timefold/solver/core/impl/util/DynamicIntArrayTest.java

Lines changed: 4 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ void defaultConstructor() {
2828
.isThrownBy(array::getLastIndex)
2929
.withMessage("Array is empty");
3030

31-
assertThat(array.length()).isZero();
3231
assertThat(array.containsIndex(0)).isFalse();
3332
assertThat(array.get(0)).isZero();
3433
}
@@ -41,8 +40,6 @@ void constructorWithMaxSize() {
4140
assertThatExceptionOfType(IllegalStateException.class)
4241
.isThrownBy(array::getFirstIndex);
4342

44-
assertThat(array.length()).isZero();
45-
4643
// Test bound checking with maxSize
4744
assertThatExceptionOfType(ArrayIndexOutOfBoundsException.class)
4845
.isThrownBy(() -> array.set(100, 5));
@@ -65,7 +62,6 @@ void setFirstElement() {
6562
assertThat(array.containsIndex(10)).isTrue();
6663
assertThat(array.containsIndex(9)).isFalse();
6764
assertThat(array.containsIndex(11)).isFalse();
68-
assertThat(array.length()).isEqualTo(11); // 0-10 inclusive
6965
}
7066

7167
@Test
@@ -81,7 +77,6 @@ void setLowerIndex() {
8177
assertThat(array.getLastIndex()).isEqualTo(10);
8278
assertThat(array.containsIndex(5)).isTrue();
8379
assertThat(array.containsIndex(10)).isTrue();
84-
assertThat(array.length()).isEqualTo(11); // 0-10 inclusive
8580
}
8681

8782
@Test
@@ -97,7 +92,6 @@ void setHigherIndex() {
9792
assertThat(array.getLastIndex()).isEqualTo(10);
9893
assertThat(array.containsIndex(5)).isTrue();
9994
assertThat(array.containsIndex(10)).isTrue();
100-
assertThat(array.length()).isEqualTo(11); // 0-10 inclusive
10195
}
10296

10397
@Test
@@ -216,34 +210,6 @@ void containsIndexOutsideRange() {
216210
}
217211
}
218212

219-
@Nested
220-
@DisplayName("Length method tests")
221-
class LengthMethodTests {
222-
223-
@Test
224-
@DisplayName("Length returns 0 for empty array")
225-
void lengthEmptyArray() {
226-
var array = new DynamicIntArray();
227-
228-
assertThat(array.length()).isZero();
229-
}
230-
231-
@Test
232-
@DisplayName("Length returns correct value after setting elements")
233-
void lengthAfterSettingElements() {
234-
var array = new DynamicIntArray();
235-
array.set(0, 1);
236-
237-
assertThat(array.length()).isEqualTo(1);
238-
239-
array.set(5, 24);
240-
assertThat(array.length()).isEqualTo(6); // 0-5 inclusive
241-
242-
array.set(10, 42);
243-
assertThat(array.length()).isEqualTo(11); // 0-10 inclusive
244-
}
245-
}
246-
247213
@Nested
248214
@DisplayName("Complex scenario tests")
249215
class ComplexScenarioTests {
@@ -288,9 +254,6 @@ void testMultipleOperations() {
288254
assertThat(array.containsIndex(15)).isTrue();
289255
assertThat(array.containsIndex(4)).isFalse();
290256
assertThat(array.containsIndex(16)).isFalse();
291-
292-
// Verify length
293-
assertThat(array.length()).isEqualTo(16); // 0-15 inclusive
294257
}
295258

296259
@Test
@@ -307,7 +270,6 @@ void testWithSparseIndices() {
307270
assertThat(array.get(10)).isEqualTo(3);
308271
assertThat(array.get(100)).isEqualTo(1);
309272
assertThat(array.get(1000)).isEqualTo(2);
310-
assertThat(array.length()).isEqualTo(1001); // 0-1000 inclusive
311273
}
312274
}
313275

@@ -322,8 +284,6 @@ void clearEmptyArray() {
322284

323285
// Should not throw an exception
324286
array.clear();
325-
326-
assertThat(array.length()).isZero();
327287
}
328288

329289
@Test
@@ -340,11 +300,8 @@ void clearResetsValues() {
340300
assertThat(array.get(10)).isZero();
341301

342302
// Array structure should be preserved
343-
assertThat(array.getFirstIndex()).isEqualTo(5);
344-
assertThat(array.getLastIndex()).isEqualTo(10);
345-
assertThat(array.containsIndex(5)).isTrue();
346-
assertThat(array.containsIndex(10)).isTrue();
347-
assertThat(array.length()).isEqualTo(11); // 0-10 inclusive
303+
assertThat(array.containsIndex(5)).isFalse();
304+
assertThat(array.containsIndex(10)).isFalse();
348305
}
349306

350307
@Test
@@ -367,30 +324,10 @@ void clearAndSetNewValues() {
367324
assertThat(array.get(10)).isZero();
368325

369326
// Array structure should be updated
370-
assertThat(array.getFirstIndex()).isEqualTo(5);
371-
assertThat(array.getLastIndex()).isEqualTo(10);
327+
assertThat(array.getFirstIndex()).isEqualTo(7);
328+
assertThat(array.getLastIndex()).isEqualTo(7);
372329
}
373330

374-
@Test
375-
@DisplayName("Clear with sparse indices")
376-
void clearWithSparseIndices() {
377-
var array = new DynamicIntArray();
378-
array.set(10, 1);
379-
array.set(100, 2);
380-
array.set(1000, 3);
381-
382-
array.clear();
383-
384-
// All values should be reset to 0
385-
assertThat(array.get(10)).isZero();
386-
assertThat(array.get(100)).isZero();
387-
assertThat(array.get(1000)).isZero();
388-
389-
// Array structure should be preserved
390-
assertThat(array.getFirstIndex()).isEqualTo(10);
391-
assertThat(array.getLastIndex()).isEqualTo(1000);
392-
assertThat(array.length()).isEqualTo(1001); // 0-1000 inclusive
393-
}
394331
}
395332

396333
}

0 commit comments

Comments
 (0)