Skip to content

Commit 81374a6

Browse files
perf: optimize DSV when used in simpler use cases (#1659)
- If only one entity has declarative variables, and all sources are either of the form "previous.*", "undirectional" or "declarative", then create a cascading updater using next to get successor and (index, id(entity)) for the initial sort. - If only one entity has declarative variables, and all sources are either of the form "next.*", "undirectional" or "declarative", then create a cascading updater using previous to get successor and (-index, id(entity)) for the initial sort. - If there are no dynamic edges, use a fixed topological order for each variable. - Otherwise use the current approach.
1 parent ce96789 commit 81374a6

31 files changed

+1487
-237
lines changed

core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.Comparator;
1919
import java.util.EnumSet;
2020
import java.util.HashMap;
21+
import java.util.HashSet;
2122
import java.util.IdentityHashMap;
2223
import java.util.LinkedHashMap;
2324
import java.util.LinkedHashSet;
@@ -1287,7 +1288,7 @@ public List<ShadowVariableDescriptor<Solution_>> getAllShadowVariableDescriptors
12871288
}
12881289

12891290
public List<DeclarativeShadowVariableDescriptor<Solution_>> getDeclarativeShadowVariableDescriptors() {
1290-
var out = new ArrayList<DeclarativeShadowVariableDescriptor<Solution_>>();
1291+
var out = new HashSet<DeclarativeShadowVariableDescriptor<Solution_>>();
12911292
for (var entityDescriptor : entityDescriptorMap.values()) {
12921293
entityDescriptor.getShadowVariableDescriptors();
12931294
for (var shadowVariableDescriptor : entityDescriptor.getShadowVariableDescriptors()) {
@@ -1296,7 +1297,7 @@ public List<DeclarativeShadowVariableDescriptor<Solution_>> getDeclarativeShadow
12961297
}
12971298
}
12981299
}
1299-
return out;
1300+
return new ArrayList<>(out);
13001301
}
13011302

13021303
public ProblemSizeStatistics getProblemSizeStatistics(Solution_ solution) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public void updateShadowVariables(Class<Solution_> solutionClass,
133133
}
134134

135135
private record InternalShadowVariableSession<Solution_>(SolutionDescriptor<Solution_> solutionDescriptor,
136-
VariableReferenceGraph<Solution_> graph) {
136+
VariableReferenceGraph graph) {
137137

138138
public static <Solution_> InternalShadowVariableSession<Solution_> build(
139139
SolutionDescriptor<Solution_> solutionDescriptor, VariableReferenceGraphBuilder<Solution_> graph,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package ai.timefold.solver.core.impl.domain.variable.declarative;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.IdentityHashMap;
6+
import java.util.LinkedHashMap;
7+
import java.util.List;
8+
import java.util.Map;
9+
import java.util.function.BiConsumer;
10+
import java.util.function.IntFunction;
11+
import java.util.stream.Collectors;
12+
13+
import ai.timefold.solver.core.impl.util.DynamicIntArray;
14+
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
15+
16+
import org.jspecify.annotations.NonNull;
17+
import org.jspecify.annotations.Nullable;
18+
19+
public abstract sealed class AbstractVariableReferenceGraph<Solution_, ChangeSet_> implements VariableReferenceGraph
20+
permits DefaultVariableReferenceGraph, FixedVariableReferenceGraph {
21+
// These structures are immutable.
22+
protected final List<EntityVariablePair<Solution_>> instanceList;
23+
protected final Map<VariableMetaModel<?, ?, ?>, Map<Object, EntityVariablePair<Solution_>>> variableReferenceToInstanceMap;
24+
protected final Map<VariableMetaModel<?, ?, ?>, List<BiConsumer<AbstractVariableReferenceGraph<Solution_, ?>, Object>>> variableReferenceToBeforeProcessor;
25+
protected final Map<VariableMetaModel<?, ?, ?>, List<BiConsumer<AbstractVariableReferenceGraph<Solution_, ?>, Object>>> variableReferenceToAfterProcessor;
26+
27+
// These structures are mutable.
28+
protected final DynamicIntArray[] edgeCount;
29+
protected final ChangeSet_ changeSet;
30+
protected final TopologicalOrderGraph graph;
31+
32+
AbstractVariableReferenceGraph(VariableReferenceGraphBuilder<Solution_> outerGraph,
33+
IntFunction<TopologicalOrderGraph> graphCreator) {
34+
instanceList = List.copyOf(outerGraph.instanceList);
35+
var instanceCount = instanceList.size();
36+
// Often the maps are a singleton; we improve performance by actually making it so.
37+
variableReferenceToInstanceMap = mapOfMapsDeepCopyOf(outerGraph.variableReferenceToInstanceMap);
38+
variableReferenceToBeforeProcessor = mapOfListsDeepCopyOf(outerGraph.variableReferenceToBeforeProcessor);
39+
variableReferenceToAfterProcessor = mapOfListsDeepCopyOf(outerGraph.variableReferenceToAfterProcessor);
40+
edgeCount = new DynamicIntArray[instanceCount];
41+
for (int i = 0; i < instanceCount; i++) {
42+
edgeCount[i] = new DynamicIntArray(instanceCount);
43+
}
44+
graph = graphCreator.apply(instanceCount);
45+
graph.withNodeData(instanceList);
46+
47+
var visited = Collections.newSetFromMap(new IdentityHashMap<>());
48+
changeSet = createChangeSet(instanceCount);
49+
for (var instance : instanceList) {
50+
var entity = instance.entity();
51+
if (visited.add(entity)) {
52+
for (var variableId : outerGraph.variableReferenceToAfterProcessor.keySet()) {
53+
afterVariableChanged(variableId, entity);
54+
}
55+
}
56+
}
57+
for (var fixedEdgeEntry : outerGraph.fixedEdges.entrySet()) {
58+
for (var toEdge : fixedEdgeEntry.getValue()) {
59+
addEdge(fixedEdgeEntry.getKey(), toEdge);
60+
}
61+
}
62+
}
63+
64+
protected abstract ChangeSet_ createChangeSet(int instanceCount);
65+
66+
public @Nullable EntityVariablePair<Solution_> lookupOrNull(VariableMetaModel<?, ?, ?> variableId, Object entity) {
67+
var map = variableReferenceToInstanceMap.get(variableId);
68+
if (map == null) {
69+
return null;
70+
}
71+
return map.get(entity);
72+
}
73+
74+
public void addEdge(@NonNull EntityVariablePair<Solution_> from, @NonNull EntityVariablePair<Solution_> to) {
75+
var fromNodeId = from.graphNodeId();
76+
var toNodeId = to.graphNodeId();
77+
if (fromNodeId == toNodeId) {
78+
return;
79+
}
80+
81+
var count = edgeCount[fromNodeId].get(toNodeId);
82+
if (count == 0) {
83+
graph.addEdge(fromNodeId, toNodeId);
84+
}
85+
edgeCount[fromNodeId].set(toNodeId, count + 1);
86+
markChanged(to);
87+
}
88+
89+
public void removeEdge(@NonNull EntityVariablePair<Solution_> from, @NonNull EntityVariablePair<Solution_> to) {
90+
var fromNodeId = from.graphNodeId();
91+
var toNodeId = to.graphNodeId();
92+
if (fromNodeId == toNodeId) {
93+
return;
94+
}
95+
96+
var count = edgeCount[fromNodeId].get(toNodeId);
97+
if (count == 1) {
98+
graph.removeEdge(fromNodeId, toNodeId);
99+
}
100+
edgeCount[fromNodeId].set(toNodeId, count - 1);
101+
markChanged(to);
102+
}
103+
104+
abstract void markChanged(EntityVariablePair<Solution_> changed);
105+
106+
@Override
107+
public void beforeVariableChanged(VariableMetaModel<?, ?, ?> variableReference, Object entity) {
108+
if (variableReference.entity().type().isInstance(entity)) {
109+
processEntity(variableReferenceToBeforeProcessor.getOrDefault(variableReference, Collections.emptyList()), entity);
110+
}
111+
}
112+
113+
@SuppressWarnings("ForLoopReplaceableByForEach")
114+
private void processEntity(List<BiConsumer<AbstractVariableReferenceGraph<Solution_, ?>, Object>> processorList,
115+
Object entity) {
116+
var processorCount = processorList.size();
117+
// Avoid creation of iterators on the hot path.
118+
// The short-lived instances were observed to cause considerable GC pressure.
119+
for (int i = 0; i < processorCount; i++) {
120+
processorList.get(i).accept(this, entity);
121+
}
122+
}
123+
124+
@Override
125+
public void afterVariableChanged(VariableMetaModel<?, ?, ?> variableReference, Object entity) {
126+
if (variableReference.entity().type().isInstance(entity)) {
127+
var node = lookupOrNull(variableReference, entity);
128+
if (node != null) {
129+
markChanged(node);
130+
}
131+
processEntity(variableReferenceToAfterProcessor.getOrDefault(variableReference, Collections.emptyList()), entity);
132+
}
133+
}
134+
135+
@Override
136+
public String toString() {
137+
var edgeList = new LinkedHashMap<EntityVariablePair<Solution_>, List<EntityVariablePair<Solution_>>>();
138+
graph.forEachEdge((from, to) -> edgeList.computeIfAbsent(instanceList.get(from), k -> new ArrayList<>())
139+
.add(instanceList.get(to)));
140+
return edgeList.entrySet()
141+
.stream()
142+
.map(e -> e.getKey() + "->" + e.getValue())
143+
.collect(Collectors.joining(
144+
"," + System.lineSeparator() + " ",
145+
"{" + System.lineSeparator() + " ",
146+
"}"));
147+
148+
}
149+
150+
@SuppressWarnings("unchecked")
151+
static <K1, K2, V> Map<K1, Map<K2, V>> mapOfMapsDeepCopyOf(Map<K1, Map<K2, V>> map) {
152+
var entryArray = map.entrySet()
153+
.stream()
154+
.map(e -> Map.entry(e.getKey(), Map.copyOf(e.getValue())))
155+
.toArray(Map.Entry[]::new);
156+
return Map.ofEntries(entryArray);
157+
}
158+
159+
@SuppressWarnings("unchecked")
160+
static <K1, V> Map<K1, List<V>> mapOfListsDeepCopyOf(Map<K1, List<V>> map) {
161+
var entryArray = map.entrySet()
162+
.stream()
163+
.map(e -> Map.entry(e.getKey(), List.copyOf(e.getValue())))
164+
.toArray(Map.Entry[]::new);
165+
return Map.ofEntries(entryArray);
166+
}
167+
168+
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55
import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor;
66
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
77

8+
import org.jspecify.annotations.Nullable;
9+
810
public record ChangedVariableNotifier<Solution_>(BiConsumer<VariableDescriptor<Solution_>, Object> beforeVariableChanged,
9-
BiConsumer<VariableDescriptor<Solution_>, Object> afterVariableChanged) {
11+
BiConsumer<VariableDescriptor<Solution_>, Object> afterVariableChanged,
12+
@Nullable InnerScoreDirector<Solution_, ?> innerScoreDirector) {
13+
1014
private static final ChangedVariableNotifier<?> EMPTY = new ChangedVariableNotifier<>((a, b) -> {
1115
},
1216
(a, b) -> {
13-
});
17+
},
18+
null);
1419

1520
@SuppressWarnings("unchecked")
1621
public static <Solution_> ChangedVariableNotifier<Solution_> empty() {
@@ -20,7 +25,8 @@ public static <Solution_> ChangedVariableNotifier<Solution_> empty() {
2025
public static <Solution_> ChangedVariableNotifier<Solution_> of(InnerScoreDirector<Solution_, ?> scoreDirector) {
2126
return new ChangedVariableNotifier<>(
2227
scoreDirector::beforeVariableChanged,
23-
scoreDirector::afterVariableChanged);
28+
scoreDirector::afterVariableChanged,
29+
scoreDirector);
2430
}
2531

2632
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
@NullMarked
1010
public final class DefaultShadowVariableSession<Solution_> implements Supply {
11-
final VariableReferenceGraph<Solution_> graph;
11+
final VariableReferenceGraph graph;
1212

13-
public DefaultShadowVariableSession(VariableReferenceGraph<Solution_> graph) {
13+
public DefaultShadowVariableSession(VariableReferenceGraph graph) {
1414
this.graph = graph;
1515
}
1616

0 commit comments

Comments
 (0)