Skip to content

Commit 5899f4f

Browse files
authored
chore: improve memory consumption of reachable values structure (#1917)
The PR updates the method used to create the reachable values structure to reduce memory usage.
1 parent d890863 commit 5899f4f

File tree

2 files changed

+176
-72
lines changed

2 files changed

+176
-72
lines changed

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableValues.java

Lines changed: 104 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package ai.timefold.solver.core.impl.heuristic.selector.common;
22

3-
import java.util.ArrayList;
3+
import java.util.AbstractList;
4+
import java.util.BitSet;
45
import java.util.Collections;
5-
import java.util.IdentityHashMap;
6-
import java.util.LinkedHashMap;
76
import java.util.List;
87
import java.util.Map;
98
import java.util.Objects;
9+
import java.util.function.Function;
1010

11-
import ai.timefold.solver.core.config.util.ConfigUtils;
1211
import ai.timefold.solver.core.impl.domain.valuerange.descriptor.FromEntityPropertyValueRangeDescriptor;
1312

1413
import org.jspecify.annotations.NullMarked;
@@ -23,14 +22,21 @@
2322
@NullMarked
2423
public final class ReachableValues {
2524

26-
private final Map<Object, ReachableItemValue> values;
25+
private final Map<Object, Integer> entitiesIndex;
26+
private final List<Object> allEntities;
27+
private final Map<Object, Integer> valuesIndex;
28+
private final List<ReachableItemValue> allValues;
2729
private final @Nullable Class<?> valueClass;
2830
private final boolean acceptsNullValue;
2931
private @Nullable ReachableItemValue firstCachedObject;
3032
private @Nullable ReachableItemValue secondCachedObject;
3133

32-
public ReachableValues(Map<Object, ReachableItemValue> values, Class<?> valueClass, boolean acceptsNullValue) {
33-
this.values = values;
34+
public ReachableValues(Map<Object, Integer> entityIndexMap, List<Object> entityList, Map<Object, Integer> valueIndexMap,
35+
List<ReachableItemValue> reachableValueList, @Nullable Class<?> valueClass, boolean acceptsNullValue) {
36+
this.entitiesIndex = entityIndexMap;
37+
this.allEntities = entityList;
38+
this.valuesIndex = valueIndexMap;
39+
this.allValues = reachableValueList;
3440
this.valueClass = valueClass;
3541
this.acceptsNullValue = acceptsNullValue;
3642
}
@@ -47,7 +53,11 @@ public ReachableValues(Map<Object, ReachableItemValue> values, Class<?> valueCla
4753
firstCachedObject = selected;
4854
}
4955
if (selected == null) {
50-
selected = values.get(value);
56+
var index = valuesIndex.get(value);
57+
if (index == null) {
58+
return null;
59+
}
60+
selected = allValues.get(index);
5161
secondCachedObject = firstCachedObject;
5262
firstCachedObject = selected;
5363
}
@@ -59,19 +69,19 @@ public List<Object> extractEntitiesAsList(Object value) {
5969
if (itemValue == null) {
6070
return Collections.emptyList();
6171
}
62-
return itemValue.randomAccessEntityList;
72+
return itemValue.getRandomAccessEntityList(allEntities);
6373
}
6474

6575
public List<Object> extractValuesAsList(Object value) {
6676
var itemValue = fetchItemValue(value);
6777
if (itemValue == null) {
6878
return Collections.emptyList();
6979
}
70-
return itemValue.randomAccessValueList;
80+
return itemValue.getRandomAccessValueList(allValues);
7181
}
7282

7383
public int getSize() {
74-
return values.size();
84+
return allValues.size();
7585
}
7686

7787
public boolean isEntityReachable(@Nullable Object origin, @Nullable Object entity) {
@@ -85,7 +95,11 @@ public boolean isEntityReachable(@Nullable Object origin, @Nullable Object entit
8595
if (originItemValue == null) {
8696
return false;
8797
}
88-
return originItemValue.entityMap.containsKey(entity);
98+
var entityIndex = entitiesIndex.get(entity);
99+
if (entityIndex == null) {
100+
throw new IllegalStateException("The entity %s is not indexed.".formatted(entity));
101+
}
102+
return originItemValue.containsEntity(entityIndex);
89103
}
90104

91105
public boolean isValueReachable(Object origin, @Nullable Object otherValue) {
@@ -96,7 +110,11 @@ public boolean isValueReachable(Object origin, @Nullable Object otherValue) {
96110
if (otherValue == null) {
97111
return acceptsNullValue;
98112
}
99-
return originItemValue.valueMap.containsKey(Objects.requireNonNull(otherValue));
113+
var otherValueIndex = valuesIndex.get(Objects.requireNonNull(otherValue));
114+
if (otherValueIndex == null) {
115+
return false;
116+
}
117+
return originItemValue.containsValue(otherValueIndex);
100118
}
101119

102120
public boolean acceptsNullValue() {
@@ -110,30 +128,88 @@ public boolean matchesValueClass(Object value) {
110128
@NullMarked
111129
public static final class ReachableItemValue {
112130
private final Object value;
113-
private final Map<Object, Object> entityMap;
114-
private final Map<Object, Object> valueMap;
115-
private final List<Object> randomAccessEntityList;
116-
private final List<Object> randomAccessValueList;
131+
private final BitSet entityBitSet;
132+
private final BitSet valueBitSet;
133+
// The entity and value list are calculated only when needed.
134+
// The goal is to avoid loading unused data upfront, as it may affect scalability.
135+
private @Nullable List<Object> onDemandRandomAccessEntityList;
136+
private @Nullable List<Object> onDemandRandomAccessValueList;
117137

118138
public ReachableItemValue(Object value, int entityListSize, int valueListSize) {
119139
this.value = value;
120-
this.entityMap = new IdentityHashMap<>(entityListSize);
121-
this.randomAccessEntityList = new ArrayList<>(entityListSize);
122-
this.valueMap = ConfigUtils.isGenericTypeImmutable(value.getClass()) ? new LinkedHashMap<>(valueListSize)
123-
: new IdentityHashMap<>(valueListSize);
124-
this.randomAccessValueList = new ArrayList<>(valueListSize);
140+
this.entityBitSet = new BitSet(entityListSize);
141+
this.valueBitSet = new BitSet(valueListSize);
142+
}
143+
144+
public void addEntity(int entityIndex) {
145+
entityBitSet.set(entityIndex);
146+
}
147+
148+
public void addValue(int valueIndex) {
149+
valueBitSet.set(valueIndex);
150+
}
151+
152+
boolean containsEntity(int entityIndex) {
153+
return entityBitSet.get(entityIndex);
125154
}
126155

127-
public void addEntity(Object entity) {
128-
if (entityMap.put(entity, entity) == null) {
129-
randomAccessEntityList.add(entity);
156+
boolean containsValue(int valueIndex) {
157+
return valueBitSet.get(valueIndex);
158+
}
159+
160+
private static int[] extractAllIndexes(BitSet bitSet) {
161+
var indexes = new int[bitSet.cardinality()];
162+
var idx = 0;
163+
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
164+
indexes[idx++] = i;
130165
}
166+
return indexes;
131167
}
132168

133-
public void addValue(Object value) {
134-
if (valueMap.put(value, value) == null) {
135-
randomAccessValueList.add(value);
169+
List<Object> getRandomAccessEntityList(List<Object> allEntities) {
170+
if (onDemandRandomAccessEntityList == null) {
171+
onDemandRandomAccessEntityList = new ArrayIndexedList<>(extractAllIndexes(entityBitSet), allEntities, null);
136172
}
173+
return onDemandRandomAccessEntityList;
174+
}
175+
176+
List<Object> getRandomAccessValueList(List<ReachableItemValue> allValues) {
177+
if (onDemandRandomAccessValueList == null) {
178+
onDemandRandomAccessValueList = new ArrayIndexedList<>(extractAllIndexes(valueBitSet), allValues, v -> v.value);
179+
}
180+
return onDemandRandomAccessValueList;
181+
}
182+
}
183+
184+
@NullMarked
185+
private static final class ArrayIndexedList<T, V> extends AbstractList<V> {
186+
187+
private final int[] valueIndex;
188+
private final List<T> allValues;
189+
private final @Nullable Function<T, V> valueExtractor;
190+
191+
private ArrayIndexedList(int[] valueIndex, List<T> allValues, @Nullable Function<T, V> valueExtractor) {
192+
this.valueIndex = valueIndex;
193+
this.allValues = allValues;
194+
this.valueExtractor = valueExtractor;
195+
}
196+
197+
@Override
198+
public V get(int index) {
199+
if (index < 0 || index >= valueIndex.length) {
200+
throw new ArrayIndexOutOfBoundsException(index);
201+
}
202+
var value = allValues.get(valueIndex[index]);
203+
if (valueExtractor == null) {
204+
return (V) value;
205+
} else {
206+
return valueExtractor.apply(value);
207+
}
208+
}
209+
210+
@Override
211+
public int size() {
212+
return valueIndex.length;
137213
}
138214
}
139215

core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java

Lines changed: 72 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package ai.timefold.solver.core.impl.score.director;
22

3+
import java.util.ArrayList;
34
import java.util.Arrays;
45
import java.util.Collections;
56
import java.util.HashMap;
67
import java.util.IdentityHashMap;
8+
import java.util.Iterator;
9+
import java.util.List;
710
import java.util.Map;
811
import java.util.Objects;
912
import java.util.function.Consumer;
@@ -33,8 +36,6 @@
3336

3437
import org.jspecify.annotations.NullMarked;
3538
import org.jspecify.annotations.Nullable;
36-
import org.slf4j.Logger;
37-
import org.slf4j.LoggerFactory;
3839

3940
/**
4041
* Caches value ranges for the current working solution,
@@ -59,8 +60,6 @@
5960
@NullMarked
6061
public final class ValueRangeManager<Solution_> {
6162

62-
private final Logger logger = LoggerFactory.getLogger(getClass());
63-
6463
private final SolutionDescriptor<Solution_> solutionDescriptor;
6564
private final CountableValueRange<?>[] fromSolution;
6665
private final ReachableValues[] reachableValues;
@@ -435,63 +434,92 @@ public ReachableValues getReachableValues(GenuineVariableDescriptor<Solution_> v
435434
}
436435
var entityDescriptor = variableDescriptor.getEntityDescriptor();
437436
var entityList = entityDescriptor.extractEntities(cachedWorkingSolution);
438-
var allValues = getFromSolution(variableDescriptor.getValueRangeDescriptor());
439-
var valuesSize = allValues.getSize();
440-
if (valuesSize > Integer.MAX_VALUE) {
437+
var entityIndexMap = buildIndexMap(entityList.iterator(), entityList.size(), false);
438+
var valueList = getFromSolution(variableDescriptor.getValueRangeDescriptor());
439+
var valueListSize = valueList.getSize();
440+
if (valueListSize > Integer.MAX_VALUE) {
441441
throw new IllegalStateException(
442-
"The matrix %s cannot be built for the entity %s (%s) because value range has a size (%d) which is higher than Integer.MAX_VALUE."
442+
"The structure %s cannot be built for the entity %s (%s) because value range has a size (%d) which is higher than Integer.MAX_VALUE."
443443
.formatted(ReachableValues.class.getSimpleName(),
444444
entityDescriptor.getEntityClass().getSimpleName(),
445-
variableDescriptor.getVariableName(), valuesSize));
445+
variableDescriptor.getVariableName(), valueListSize));
446+
}
447+
Class<?> valueClass = findValueClass(valueList);
448+
var valueIndexMap = buildIndexMap(valueList.createOriginalIterator(), (int) valueListSize,
449+
ConfigUtils.isGenericTypeImmutable(valueClass));
450+
var reachableValueList = initReachableValueList(valueList, entityList.size());
451+
for (var i = 0; i < entityList.size(); i++) {
452+
var entity = entityList.get(i);
453+
var valueRange = getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity);
454+
loadEntityValueRange(i, valueIndexMap, valueRange, reachableValueList);
446455
}
456+
values = new ReachableValues(entityIndexMap, entityList, valueIndexMap, reachableValueList, valueClass,
457+
variableDescriptor.getValueRangeDescriptor().acceptsNullInValueRange());
458+
reachableValues[variableDescriptor.getValueRangeDescriptor().getOrdinal()] = values;
459+
return values;
460+
}
461+
462+
private static <T> @Nullable Class<?> findValueClass(CountableValueRange<T> valueRange) {
447463
Class<?> valueClass = null;
448464
var idx = 0;
449-
while (valueClass == null && idx < allValues.getSize()) {
450-
var value = allValues.get(idx++);
465+
while (idx < valueRange.getSize()) {
466+
var value = valueRange.get(idx++);
451467
if (value == null) {
452468
continue;
453469
}
454470
valueClass = value.getClass();
455471
break;
456472
}
457-
Map<Object, ReachableItemValue> reachableValuesMap = ConfigUtils.isGenericTypeImmutable(valueClass)
458-
? new HashMap<>((int) valuesSize)
459-
: new IdentityHashMap<>((int) valuesSize);
460-
for (var entity : entityList) {
461-
var range = getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity);
462-
for (var i = 0; i < range.getSize(); i++) {
463-
var value = range.get(i);
464-
if (value == null) {
465-
continue;
466-
}
467-
var item = initReachableMap(reachableValuesMap, value, entityList.size(), (int) valuesSize);
468-
item.addEntity(entity);
469-
for (int j = i + 1; j < range.getSize(); j++) {
470-
var otherValue = range.get(j);
471-
if (otherValue == null) {
472-
continue;
473-
}
474-
item.addValue(otherValue);
475-
var otherValueItem =
476-
initReachableMap(reachableValuesMap, otherValue, entityList.size(), (int) valuesSize);
477-
otherValueItem.addValue(value);
478-
}
473+
return valueClass;
474+
}
475+
476+
private static Map<Object, Integer> buildIndexMap(Iterator<@Nullable Object> allValues, int size, boolean isImmutable) {
477+
Map<Object, Integer> indexMap = isImmutable ? new HashMap<>(size) : new IdentityHashMap<>(size);
478+
var idx = 0;
479+
while (allValues.hasNext()) {
480+
var value = allValues.next();
481+
if (value == null) {
482+
continue;
479483
}
484+
indexMap.put(value, idx++);
480485
}
481-
values = new ReachableValues(reachableValuesMap, valueClass,
482-
variableDescriptor.getValueRangeDescriptor().acceptsNullInValueRange());
483-
reachableValues[variableDescriptor.getValueRangeDescriptor().getOrdinal()] = values;
484-
return values;
486+
return indexMap;
485487
}
486488

487-
private static ReachableItemValue initReachableMap(Map<Object, ReachableItemValue> reachableValuesMap, Object value,
488-
int entityListSize, int valueListSize) {
489-
var item = reachableValuesMap.get(value);
490-
if (item == null) {
491-
item = new ReachableItemValue(value, entityListSize, valueListSize);
492-
reachableValuesMap.put(value, item);
489+
private static List<ReachableItemValue> initReachableValueList(CountableValueRange<Object> valueRange, int entityListSize) {
490+
var size = (int) valueRange.getSize();
491+
var valueList = new ArrayList<ReachableItemValue>(size);
492+
for (var i = 0; i < size; i++) {
493+
var value = valueRange.get(i);
494+
if (value == null) {
495+
continue;
496+
}
497+
valueList.add(new ReachableItemValue(value, entityListSize, size));
498+
}
499+
return valueList;
500+
}
501+
502+
private static <T> void loadEntityValueRange(int entityIndex, Map<Object, Integer> valueIndexMap,
503+
CountableValueRange<T> valueRange, List<ReachableItemValue> reachableValueList) {
504+
for (var i = 0; i < valueRange.getSize(); i++) {
505+
var value = valueRange.get(i);
506+
if (value == null) {
507+
continue;
508+
}
509+
var valueIndex = valueIndexMap.get(value);
510+
var item = reachableValueList.get(valueIndex);
511+
item.addEntity(entityIndex);
512+
for (var j = i + 1; j < valueRange.getSize(); j++) {
513+
var otherValue = valueRange.get(j);
514+
if (otherValue == null) {
515+
continue;
516+
}
517+
var otherValueIndex = valueIndexMap.get(otherValue);
518+
var otherValueItem = reachableValueList.get(otherValueIndex);
519+
item.addValue(otherValueIndex);
520+
otherValueItem.addValue(valueIndex);
521+
}
493522
}
494-
return item;
495523
}
496524

497525
public void reset(@Nullable Solution_ workingSolution) {

0 commit comments

Comments
 (0)