Skip to content

Commit 779406c

Browse files
committed
Remove the use of a massive array
DefaultEdge[][] took 2GB of heap per thread in mid-size MJS problems. Even though it did improve performance, such a hit would have made multi-threading practically impossible. It would have also prevented us from solving large problems.
1 parent 92e7832 commit 779406c

5 files changed

Lines changed: 73 additions & 116 deletions

File tree

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

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,29 @@ public DefaultTopologicalOrderGraph(final int size) {
3232
}
3333

3434
@Override
35-
public void addEdge(Edge edge) {
36-
var from = edge.from();
37-
var to = edge.to();
38-
forwardEdges[from].add(to);
39-
backEdges[to].add(from);
35+
public void addEdge(int fromNode, int toNode) {
36+
forwardEdges[fromNode].add(toNode);
37+
backEdges[toNode].add(fromNode);
4038
}
4139

4240
@Override
43-
public void removeEdge(Edge edge) {
44-
var from = edge.from();
45-
var to = edge.to();
46-
forwardEdges[from].remove(to);
47-
backEdges[to].remove(from);
41+
public void removeEdge(int fromNode, int toNode) {
42+
forwardEdges[fromNode].remove(toNode);
43+
backEdges[toNode].remove(fromNode);
4844
}
4945

5046
@Override
51-
public PrimitiveIterator.OfInt nodeForwardEdges(int from) {
52-
return componentMap.get(from).stream()
47+
public void forEachEdge(EdgeConsumer edgeConsumer) {
48+
for (var fromNode = 0; fromNode < forwardEdges.length; fromNode++) {
49+
for (var toNode : forwardEdges[fromNode]) {
50+
edgeConsumer.accept(fromNode, toNode);
51+
}
52+
}
53+
}
54+
55+
@Override
56+
public PrimitiveIterator.OfInt nodeForwardEdges(int fromNode) {
57+
return componentMap.get(fromNode).stream()
5358
.flatMap(member -> forwardEdges[member].stream())
5459
.mapToInt(Integer::intValue)
5560
.distinct().iterator();

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

Lines changed: 35 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import java.util.BitSet;
55
import java.util.Collections;
66
import java.util.IdentityHashMap;
7+
import java.util.LinkedHashMap;
78
import java.util.List;
89
import java.util.Map;
910
import java.util.function.BiConsumer;
1011
import java.util.function.Consumer;
1112
import java.util.function.IntFunction;
13+
import java.util.stream.Collectors;
1214

1315
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
1416

@@ -24,7 +26,7 @@ final class DefaultVariableReferenceGraph<Solution_> implements VariableReferenc
2426
private final Map<VariableMetaModel<?, ?, ?>, List<BiConsumer<VariableReferenceGraph<Solution_>, Object>>> variableReferenceToAfterProcessor;
2527

2628
// These structures are mutable.
27-
private final DefaultEdge[][] edges;
29+
private final BitSet[] edgesFromNode;
2830
private final TopologicalOrderGraph graph;
2931
private final BitSet changed;
3032

@@ -38,7 +40,7 @@ public DefaultVariableReferenceGraph(VariableReferenceGraphBuilder<Solution_> ou
3840
variableReferenceToInstanceMap = mapOfMapsDeepCopyOf(outerGraph.variableReferenceToInstanceMap);
3941
variableReferenceToBeforeProcessor = mapOfListsDeepCopyOf(outerGraph.variableReferenceToBeforeProcessor);
4042
variableReferenceToAfterProcessor = mapOfListsDeepCopyOf(outerGraph.variableReferenceToAfterProcessor);
41-
edges = new DefaultEdge[instanceCount][instanceCount];
43+
edgesFromNode = new BitSet[instanceCount];
4244
graph = graphCreator.apply(instanceCount);
4345
graph.withNodeData(instanceList);
4446
changed = new BitSet(instanceCount);
@@ -82,15 +84,20 @@ public void addEdge(@NonNull EntityVariablePair<Solution_> from, @NonNull Entity
8284
return;
8385
}
8486

85-
var edge = edges[fromNodeId][toNodeId];
86-
if (edge == null) {
87-
edge = new DefaultEdge(fromNodeId, toNodeId);
88-
edges[fromNodeId][toNodeId] = edge;
89-
graph.addEdge(edge);
87+
var hasEdge = false;
88+
var presentEdges = edgesFromNode[fromNodeId];
89+
if (presentEdges == null) {
90+
presentEdges = new BitSet(edgesFromNode.length);
91+
edgesFromNode[fromNodeId] = presentEdges;
92+
} else {
93+
hasEdge = presentEdges.get(toNodeId);
9094
}
91-
edge.increaseCount();
9295

93-
markChanged(to);
96+
if (!hasEdge) {
97+
presentEdges.set(toNodeId);
98+
graph.addEdge(fromNodeId, toNodeId);
99+
markChanged(to);
100+
}
94101
}
95102

96103
@Override
@@ -101,13 +108,15 @@ public void removeEdge(@NonNull EntityVariablePair<Solution_> from, @NonNull Ent
101108
return;
102109
}
103110

104-
var edge = edges[fromNodeId][toNodeId];
105-
if (edge.decreaseCount() == 0) {
106-
graph.removeEdge(edge);
107-
edges[fromNodeId][toNodeId] = null;
111+
var presentEdges = edgesFromNode[fromNodeId];
112+
if (presentEdges == null) {
113+
return;
114+
}
115+
if (presentEdges.get(toNodeId)) {
116+
graph.removeEdge(fromNodeId, toNodeId);
117+
presentEdges.clear(toNodeId);
118+
markChanged(to);
108119
}
109-
110-
markChanged(to);
111120
}
112121

113122
@Override
@@ -153,28 +162,17 @@ public void afterVariableChanged(VariableMetaModel<?, ?, ?> variableReference, O
153162

154163
@Override
155164
public String toString() {
156-
var builder = new StringBuilder("{\n");
157-
for (int from = 0; from < edges.length; from++) {
158-
var row = edges[from];
159-
var first = true;
160-
for (int to = 0; to < row.length; to++) {
161-
var edge = row[to];
162-
if (edge != null) {
163-
if (first) {
164-
first = false;
165-
builder.append(" \"").append(instanceList.get(from)).append("\": [");
166-
} else {
167-
builder.append(", ");
168-
}
169-
builder.append("\"%s\"".formatted(instanceList.get(to)));
170-
}
171-
}
172-
if (!first) {
173-
builder.append("],\n");
174-
}
175-
}
176-
builder.append("}");
177-
return builder.toString();
165+
var edgeList = new LinkedHashMap<EntityVariablePair<Solution_>, List<EntityVariablePair<Solution_>>>();
166+
graph.forEachEdge((from, to) -> edgeList.computeIfAbsent(instanceList.get(from), k -> new ArrayList<>())
167+
.add(instanceList.get(to)));
168+
return edgeList.entrySet()
169+
.stream()
170+
.map(e -> e.getKey() + "->" + e.getValue())
171+
.collect(Collectors.joining(
172+
" ," + System.lineSeparator(),
173+
"{" + System.lineSeparator(),
174+
"}"));
175+
178176
}
179177

180178
@SuppressWarnings("unchecked")
@@ -195,54 +193,4 @@ private static <K1, V> Map<K1, List<V>> mapOfListsDeepCopyOf(Map<K1, List<V>> ma
195193
return Map.ofEntries(entryArray);
196194
}
197195

198-
public static final class DefaultEdge implements TopologicalOrderGraph.Edge {
199-
200-
private final int from;
201-
private final int to;
202-
private int count = 0;
203-
204-
public DefaultEdge(int from, int to) {
205-
this.from = from;
206-
this.to = to;
207-
}
208-
209-
@Override
210-
public int from() {
211-
return from;
212-
}
213-
214-
@Override
215-
public int to() {
216-
return to;
217-
}
218-
219-
public void increaseCount() {
220-
count++;
221-
}
222-
223-
public int decreaseCount() {
224-
return --count;
225-
}
226-
227-
@Override
228-
public boolean equals(Object o) {
229-
return o instanceof DefaultEdge other &&
230-
from == other.from &&
231-
to == other.to;
232-
}
233-
234-
@Override
235-
public int hashCode() {
236-
var hash = 31;
237-
hash += 31 * from;
238-
hash += 31 * to * to; // Make sure order of nodes matters.
239-
return hash;
240-
}
241-
242-
@Override
243-
public String toString() {
244-
return "%d->%d".formatted(from, to);
245-
}
246-
}
247-
248196
}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ default <Solution_> void withNodeData(List<EntityVariablePair<Solution_>> nodes)
2727
* {@link #getTopologicalOrder(int)} is allowed to be invalid
2828
* when this method returns.
2929
*/
30-
void addEdge(Edge edge);
30+
void addEdge(int from, int to);
3131

3232
/**
3333
* Called when a graph edge is removed.
@@ -36,13 +36,14 @@ default <Solution_> void withNodeData(List<EntityVariablePair<Solution_>> nodes)
3636
* {@link #getTopologicalOrder(int)} is allowed to be invalid
3737
* when this method returns.
3838
*/
39-
void removeEdge(Edge edge);
39+
void removeEdge(int from, int to);
4040

41-
interface Edge {
41+
void forEachEdge(EdgeConsumer edgeConsumer);
4242

43-
int from();
43+
@FunctionalInterface
44+
interface EdgeConsumer {
4445

45-
int to();
46+
void accept(int from, int to);
4647

4748
}
4849

core/src/test/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupportTest.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -191,19 +191,17 @@ public void removeEdge(VariableMetaModel<?, ?, ?> fromId, Object fromEntity, Var
191191
}
192192

193193
@Override
194-
public void addEdge(Edge edge) {
195-
super.addEdge(edge);
196-
var from = edge.from();
197-
var to = edge.to();
198-
addEdge(nodeToVariableMetamodel[from], nodeToEntities[from], nodeToVariableMetamodel[to], nodeToEntities[to]);
194+
public void addEdge(int fromNode, int toNode) {
195+
super.addEdge(fromNode, toNode);
196+
addEdge(nodeToVariableMetamodel[fromNode], nodeToEntities[fromNode], nodeToVariableMetamodel[toNode],
197+
nodeToEntities[toNode]);
199198
}
200199

201200
@Override
202-
public void removeEdge(Edge edge) {
203-
super.addEdge(edge);
204-
var from = edge.from();
205-
var to = edge.to();
206-
removeEdge(nodeToVariableMetamodel[from], nodeToEntities[from], nodeToVariableMetamodel[to], nodeToEntities[to]);
201+
public void removeEdge(int fromNode, int toNode) {
202+
super.removeEdge(fromNode, toNode);
203+
removeEdge(nodeToVariableMetamodel[fromNode], nodeToEntities[fromNode], nodeToVariableMetamodel[toNode],
204+
nodeToEntities[toNode]);
207205
}
208206
}
209207

core/src/test/java/ai/timefold/solver/core/testdomain/declarative/dependency/TestdataDependencyConstraintProvider.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ public class TestdataDependencyConstraintProvider implements ConstraintProvider
1919
.asConstraint("Invalid task"),
2020
constraintFactory.forEach(TestdataDependencyValue.class)
2121
.filter(task -> !task.isInvalid())
22-
.penalize(HardSoftScore.ONE_SOFT, t -> (int) Duration.between(t.getEntity().getStartTime(),
23-
t.getEndTime()).toMinutes())
22+
.penalize(HardSoftScore.ONE_SOFT, t -> {
23+
if (t.getEndTime() == null) {
24+
throw new IllegalStateException();
25+
}
26+
return (int) Duration.between(t.getEntity().getStartTime(),
27+
t.getEndTime()).toMinutes();
28+
})
2429
.asConstraint("Finish tasks as early as possible")
2530
};
2631
}

0 commit comments

Comments
 (0)