Skip to content

Commit b96aa54

Browse files
authored
perf: improve runtime performance of exhaustive search (#2214)
1 parent 6a049ca commit b96aa54

21 files changed

+247
-196
lines changed

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ public void solve(SolverScope<Solution_> solverScope) {
6363

6464
while (!expandableNodeQueue.isEmpty() && !phaseTermination.isPhaseTerminated(phaseScope)) {
6565
var stepScope = new ExhaustiveSearchStepScope<>(phaseScope);
66-
var node = expandableNodeQueue.last();
67-
expandableNodeQueue.remove(node);
66+
var node = expandableNodeQueue.removeLast();
6867
stepScope.setExpandingNode(node);
6968
stepStarted(stepScope);
7069
decider.restoreWorkingSolution(stepScope, assertWorkingSolutionScoreFromScratch,

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
2727
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
2828
import ai.timefold.solver.core.impl.exhaustivesearch.decider.AbstractExhaustiveSearchDecider;
29-
import ai.timefold.solver.core.impl.exhaustivesearch.decider.BasicExhaustiveSearchDecider;
29+
import ai.timefold.solver.core.impl.exhaustivesearch.decider.BasicVariableExhaustiveSearchDecider;
3030
import ai.timefold.solver.core.impl.exhaustivesearch.decider.ListVariableExhaustiveSearchDecider;
3131
import ai.timefold.solver.core.impl.exhaustivesearch.decider.MixedVariableExhaustiveSearchDecider;
3232
import ai.timefold.solver.core.impl.exhaustivesearch.node.bounder.TrendBasedScoreBounder;
@@ -195,7 +195,7 @@ protected EntityDescriptor<Solution_> deduceEntityDescriptor(SolutionDescriptor<
195195
termination, sourceEntitySelector, manualEntityMimicRecorder,
196196
new MoveSelectorBasedMoveRepository<>(moveSelector), scoreBounderEnabled, scoreBounder);
197197
} else {
198-
decider = new BasicExhaustiveSearchDecider<>(configPolicy.getLogIndentation(), bestSolutionRecaller,
198+
decider = new BasicVariableExhaustiveSearchDecider<>(configPolicy.getLogIndentation(), bestSolutionRecaller,
199199
termination, sourceEntitySelector, manualEntityMimicRecorder,
200200
new MoveSelectorBasedMoveRepository<>(moveSelector), scoreBounderEnabled, scoreBounder);
201201

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424

2525
public abstract sealed class AbstractExhaustiveSearchDecider<Solution_, Score_ extends Score<Score_>>
2626
implements ExhaustiveSearchPhaseLifecycleListener<Solution_>
27-
permits BasicExhaustiveSearchDecider, ListVariableExhaustiveSearchDecider, MixedVariableExhaustiveSearchDecider {
27+
permits BasicVariableExhaustiveSearchDecider, ListVariableExhaustiveSearchDecider,
28+
MixedVariableExhaustiveSearchDecider {
2829

2930
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExhaustiveSearchDecider.class);
3031

@@ -123,9 +124,8 @@ protected void doMove(ExhaustiveSearchStepScope<Solution_> stepScope, Exhaustive
123124
}
124125
}
125126
var nodeScore = moveNode.getScore();
126-
LOGGER.trace("{} Move treeId ({}), score ({}), expandable ({}), move ({}).",
127-
logIndentation, executionPoint.treeId(), nodeScore == null ? "null" : nodeScore, moveNode.isExpandable(),
128-
moveNode.getMove());
127+
LOGGER.trace("{} Move treeId ({}), score ({}), move ({}).",
128+
logIndentation, executionPoint.treeId(), nodeScore == null ? "null" : nodeScore, moveNode.getMove());
129129
}
130130

131131
private void processMove(ExhaustiveSearchStepScope<Solution_> stepScope,
@@ -204,7 +204,7 @@ protected void fillLayerList(ExhaustiveSearchPhaseScope<Solution_> phaseScope) {
204204

205205
protected void initStartNode(ExhaustiveSearchPhaseScope<Solution_> phaseScope,
206206
ExhaustiveSearchLayer layer) {
207-
var startLayer = layer == null ? phaseScope.getLayerList().get(0) : layer;
207+
var startLayer = layer == null ? phaseScope.getLayerList().getFirst() : layer;
208208
var startNode = new ExhaustiveSearchNode<Solution_>(startLayer, null);
209209

210210
if (scoreBounderEnabled) {

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/BasicExhaustiveSearchDecider.java renamed to core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/BasicVariableExhaustiveSearchDecider.java

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

3-
import java.util.ArrayList;
4-
import java.util.Collections;
3+
import java.util.Arrays;
54

65
import ai.timefold.solver.core.api.score.Score;
76
import ai.timefold.solver.core.impl.exhaustivesearch.node.ExhaustiveSearchNode;
@@ -16,10 +15,10 @@
1615
import ai.timefold.solver.core.preview.api.move.Move;
1716
import ai.timefold.solver.core.preview.api.move.builtin.Moves;
1817

19-
public final class BasicExhaustiveSearchDecider<Solution_, Score_ extends Score<Score_>>
18+
public final class BasicVariableExhaustiveSearchDecider<Solution_, Score_ extends Score<Score_>>
2019
extends AbstractExhaustiveSearchDecider<Solution_, Score_> {
2120

22-
public BasicExhaustiveSearchDecider(String logIndentation, BestSolutionRecaller<Solution_> bestSolutionRecaller,
21+
public BasicVariableExhaustiveSearchDecider(String logIndentation, BestSolutionRecaller<Solution_> bestSolutionRecaller,
2322
PhaseTermination<Solution_> termination, EntitySelector<Solution_> sourceEntitySelector,
2423
ManualEntityMimicRecorder<Solution_> manualEntityMimicRecorder, MoveRepository<Solution_> moveRepository,
2524
boolean scoreBounderEnabled, ScoreBounder<?> scoreBounder) {
@@ -65,34 +64,39 @@ public boolean isEntityReinitializable(Object entity) {
6564
return reinitializeVariableCount > 0;
6665
}
6766

67+
@SuppressWarnings("unchecked")
6868
@Override
6969
public void restoreWorkingSolution(ExhaustiveSearchStepScope<Solution_> stepScope,
7070
boolean assertWorkingSolutionScoreFromScratch, boolean assertExpectedWorkingSolutionScore) {
7171
var phaseScope = stepScope.getPhaseScope();
7272
var oldNode = phaseScope.getLastCompletedStepScope().getExpandingNode();
7373
var newNode = stepScope.getExpandingNode();
74-
var oldMoveList = new ArrayList<Move<Solution_>>(oldNode.getDepth());
75-
var newMoveList = new ArrayList<Move<Solution_>>(newNode.getDepth());
74+
var oldMoveArray = new Move[oldNode.getDepth()];
75+
var newMoveArray = new Move[newNode.getDepth()];
76+
var oldMoveCount = 0;
77+
var newMoveCount = 0;
7678
while (oldNode != newNode) {
7779
var oldDepth = oldNode.getDepth();
7880
var newDepth = newNode.getDepth();
7981
if (oldDepth < newDepth) {
80-
newMoveList.add(newNode.getMove());
82+
newMoveArray[newMoveArray.length - newMoveCount++ - 1] = newNode.getMove(); // Build this in reverse.
8183
newNode = newNode.getParent();
8284
} else {
83-
oldMoveList.add(oldNode.getUndoMove());
85+
oldMoveArray[oldMoveCount++] = oldNode.getUndoMove();
8486
oldNode = oldNode.getParent();
8587
}
8688
}
87-
var restoreMoveList = new ArrayList<Move<Solution_>>(oldMoveList.size() + newMoveList.size());
88-
restoreMoveList.addAll(oldMoveList);
89-
Collections.reverse(newMoveList);
90-
restoreMoveList.addAll(newMoveList);
91-
if (restoreMoveList.isEmpty()) {
92-
// No moves to restore, so the working solution is already correct
89+
var totalCount = newMoveCount + oldMoveCount;
90+
if (totalCount == 0) {
91+
// No moves to restore, so the working solution is already correct.
9392
return;
9493
}
94+
// Build a composite move of both arrays.
95+
var moves = Arrays.copyOf(oldMoveArray, totalCount);
96+
System.arraycopy(newMoveArray, newMoveArray.length - newMoveCount, moves, oldMoveCount, newMoveCount);
97+
var restoreMoveList = Arrays.<Move<Solution_>> asList(moves);
9598
var compositeMove = Moves.compose(restoreMoveList);
99+
// Execute the move.
96100
phaseScope.getScoreDirector().executeMove(compositeMove);
97101
var startingStepScore = stepScope.<Score_> getStartingStepScore();
98102
phaseScope.getSolutionDescriptor().setScore(phaseScope.getWorkingSolution(),

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ListVariableExhaustiveSearchDecider.java

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package ai.timefold.solver.core.impl.exhaustivesearch.decider;
22

3-
import java.util.ArrayList;
4-
import java.util.Collections;
3+
import java.util.Arrays;
54

65
import ai.timefold.solver.core.api.score.Score;
76
import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply;
@@ -102,35 +101,56 @@ public boolean isEntityReinitializable(Object entity) {
102101
public void restoreWorkingSolution(ExhaustiveSearchStepScope<Solution_> stepScope,
103102
boolean assertWorkingSolutionScoreFromScratch, boolean assertExpectedWorkingSolutionScore) {
104103
var phaseScope = stepScope.getPhaseScope();
105-
//First, undo all previous changes
106-
var undoNode = phaseScope.getLastCompletedStepScope().getExpandingNode();
107-
var unassignMoveList = new ArrayList<Move<Solution_>>();
108-
while (undoNode.getUndoMove() != null) {
109-
unassignMoveList.add(undoNode.getUndoMove());
110-
undoNode = undoNode.getParent();
111-
}
112-
// Next, rebuild the solution starting from the current search element
113-
var assignNode = stepScope.getExpandingNode();
114-
var assignMoveList = new ArrayList<Move<Solution_>>();
115-
while (assignNode.getMove() != null) {
116-
assignMoveList.add(assignNode.getMove());
117-
assignNode = assignNode.getParent();
118-
}
119-
Collections.reverse(assignMoveList);
120-
var allMoves = new ArrayList<Move<Solution_>>(unassignMoveList.size() + assignMoveList.size());
121-
allMoves.addAll(unassignMoveList);
122-
allMoves.addAll(assignMoveList);
123-
if (allMoves.isEmpty()) {
124-
// No moves to restore, so the working solution is already correct
104+
//First, undo all previous changes.
105+
var unassignMoves = listAllUndoMoves(phaseScope.getLastCompletedStepScope().getExpandingNode());
106+
// Next, rebuild the solution starting from the current search element.
107+
var assignMoves = listAllMovesInReverseOrder(stepScope.getExpandingNode());
108+
var totalLength = unassignMoves.length + assignMoves.length;
109+
if (totalLength == 0) {
110+
// No moves to restore, so the working solution is already correct.
125111
return;
126112
}
127-
var compositeMove = Moves.compose(allMoves);
113+
// Build a composite move of both arrays.
114+
var moves = Arrays.copyOf(unassignMoves, unassignMoves.length + assignMoves.length);
115+
System.arraycopy(assignMoves, 0, moves, unassignMoves.length, assignMoves.length);
116+
var compositeMove = Moves.compose(moves);
117+
// Execute the move.
128118
phaseScope.getScoreDirector().executeMove(compositeMove);
129119
var score = phaseScope.<Score_> calculateScore();
130120
stepScope.getExpandingNode().setScore(score);
131121
phaseScope.getSolutionDescriptor().setScore(phaseScope.getWorkingSolution(), score.raw());
132122
}
133123

124+
private static <Solution_> Move<Solution_>[] listAllUndoMoves(ExhaustiveSearchNode<Solution_> node) {
125+
return listAllUndoMoves(node, 0);
126+
}
127+
128+
private static <Solution_> Move<Solution_>[] listAllUndoMoves(ExhaustiveSearchNode<Solution_> node, int depth) {
129+
var undoMove = node.getUndoMove();
130+
if (undoMove != null) {
131+
var array = listAllUndoMoves(node.getParent(), depth + 1);
132+
array[depth] = undoMove;
133+
return array;
134+
} else {
135+
return new Move[depth];
136+
}
137+
}
138+
139+
private static <Solution_> Move<Solution_>[] listAllMovesInReverseOrder(ExhaustiveSearchNode<Solution_> node) {
140+
return listAllMovesInReverseOrder(node, 0);
141+
}
142+
143+
private static <Solution_> Move<Solution_>[] listAllMovesInReverseOrder(ExhaustiveSearchNode<Solution_> node, int depth) {
144+
var move = node.getMove();
145+
if (move != null) {
146+
var array = listAllMovesInReverseOrder(node.getParent(), depth + 1);
147+
array[array.length - depth - 1] = move;
148+
return array;
149+
} else {
150+
return new Move[depth];
151+
}
152+
}
153+
134154
// ************************************************************************
135155
// Lifecycle methods
136156
// ************************************************************************

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/ExhaustiveSearchNode.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ public class ExhaustiveSearchNode<Solution_> {
2222
* @see ScoreBounder#calculateOptimisticBound(ScoreDirector, InnerScore)
2323
*/
2424
private InnerScore<?> optimisticBound;
25-
private boolean expandable = false;
2625

2726
public ExhaustiveSearchNode(ExhaustiveSearchLayer layer, ExhaustiveSearchNode<Solution_> parent) {
2827
this.layer = layer;
@@ -80,14 +79,6 @@ public void setOptimisticBound(InnerScore<?> optimisticBound) {
8079
this.optimisticBound = optimisticBound;
8180
}
8281

83-
public boolean isExpandable() {
84-
return expandable;
85-
}
86-
87-
public void setExpandable(boolean expandable) {
88-
this.expandable = expandable;
89-
}
90-
9182
// ************************************************************************
9283
// Calculated methods
9384
// ************************************************************************

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/BreadthFirstNodeComparator.java

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,22 @@ public BreadthFirstNodeComparator(boolean scoreBounderEnabled) {
2727
@Override
2828
public int compare(ExhaustiveSearchNode<Solution_> a, ExhaustiveSearchNode<Solution_> b) {
2929
// Investigate shallower nodes first
30-
var aDepth = a.getDepth();
31-
var bDepth = b.getDepth();
32-
if (aDepth < bDepth) {
33-
return 1;
34-
} else if (aDepth > bDepth) {
35-
return -1;
30+
var depthComparison = Integer.compare(a.getDepth(), b.getDepth());
31+
if (depthComparison != 0) {
32+
return -depthComparison;
3633
}
3734
// Investigate better score first (ignore initScore to avoid depth first ordering)
3835
Score aScore = a.getScore().raw();
3936
Score bScore = b.getScore().raw();
4037
var scoreComparison = aScore.compareTo(bScore);
41-
if (scoreComparison < 0) {
42-
return -1;
43-
} else if (scoreComparison > 0) {
44-
return 1;
38+
if (scoreComparison != 0) {
39+
return scoreComparison;
4540
}
4641
if (scoreBounderEnabled) {
4742
// Investigate better optimistic bound first
4843
var optimisticBoundComparison = a.getOptimisticBound().compareTo(b.getOptimisticBound());
49-
if (optimisticBoundComparison < 0) {
50-
return -1;
51-
} else if (optimisticBoundComparison > 0) {
52-
return 1;
44+
if (optimisticBoundComparison != 0) {
45+
return optimisticBoundComparison;
5346
}
5447
}
5548
// No point to investigating higher parent breadth index first (no impact on the churn on workingSolution)

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/DepthFirstNodeComparator.java

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,16 @@ public DepthFirstNodeComparator(boolean scoreBounderEnabled) {
2020
@Override
2121
public int compare(ExhaustiveSearchNode<Solution_> a, ExhaustiveSearchNode<Solution_> b) {
2222
// Investigate deeper first
23-
var aDepth = a.getDepth();
24-
var bDepth = b.getDepth();
25-
if (aDepth < bDepth) {
26-
return -1;
27-
} else if (aDepth > bDepth) {
28-
return 1;
23+
var depthComparison = Integer.compare(a.getDepth(), b.getDepth());
24+
if (depthComparison != 0) {
25+
return depthComparison;
2926
}
3027
// Investigate better score first (ignore initScore as that's already done by investigate deeper first)
3128
Score aScore = a.getScore().raw();
3229
Score bScore = b.getScore().raw();
3330
var scoreComparison = aScore.compareTo(bScore);
34-
if (scoreComparison < 0) {
35-
return -1;
36-
} else if (scoreComparison > 0) {
37-
return 1;
31+
if (scoreComparison != 0) {
32+
return scoreComparison;
3833
}
3934
// Pitfall: score is compared before optimisticBound, because of this mixed ONLY_UP and ONLY_DOWN cases:
4035
// - Node a has score 0hard/20medium/-50soft and optimisticBound 0hard/+(infinity)medium/-50soft
@@ -43,19 +38,14 @@ public int compare(ExhaustiveSearchNode<Solution_> a, ExhaustiveSearchNode<Solut
4338
if (scoreBounderEnabled) {
4439
// Investigate better optimistic bound first
4540
var optimisticBoundComparison = a.getOptimisticBound().compareTo(b.getOptimisticBound());
46-
if (optimisticBoundComparison < 0) {
47-
return -1;
48-
} else if (optimisticBoundComparison > 0) {
49-
return 1;
41+
if (optimisticBoundComparison != 0) {
42+
return optimisticBoundComparison;
5043
}
5144
}
5245
// Investigate higher parent breadth index first (to reduce on the churn on workingSolution)
53-
var aParentBreadth = a.getParentBreadth();
54-
var bParentBreadth = b.getParentBreadth();
55-
if (aParentBreadth < bParentBreadth) {
56-
return -1;
57-
} else if (aParentBreadth > bParentBreadth) {
58-
return 1;
46+
var parentBreadthComparison = Long.compare(a.getParentBreadth(), b.getParentBreadth());
47+
if (parentBreadthComparison != 0) {
48+
return parentBreadthComparison;
5949
}
6050
// Investigate lower breadth index first (to respect ValueSortingManner)
6151
return Long.compare(b.getBreadth(), a.getBreadth());

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/OptimisticBoundFirstNodeComparator.java

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,18 @@ public int compare(ExhaustiveSearchNode<Solution_> a, ExhaustiveSearchNode<Solut
3131
Score aScore = a.getScore().raw();
3232
Score bScore = b.getScore().raw();
3333
var scoreComparison = aScore.compareTo(bScore);
34-
if (scoreComparison < 0) {
35-
return -1;
36-
} else if (scoreComparison > 0) {
37-
return 1;
34+
if (scoreComparison != 0) {
35+
return scoreComparison;
3836
}
3937
// Investigate deeper first
40-
var aDepth = a.getDepth();
41-
var bDepth = b.getDepth();
42-
if (aDepth < bDepth) {
43-
return -1;
44-
} else if (aDepth > bDepth) {
45-
return 1;
38+
var depthComparison = Integer.compare(a.getDepth(), b.getDepth());
39+
if (depthComparison != 0) {
40+
return depthComparison;
4641
}
4742
// Investigate higher parent breadth index first (to reduce on the churn on workingSolution)
48-
var aParentBreadth = a.getParentBreadth();
49-
var bParentBreadth = b.getParentBreadth();
50-
if (aParentBreadth < bParentBreadth) {
51-
return -1;
52-
} else if (aParentBreadth > bParentBreadth) {
53-
return 1;
43+
var parentBreadthComparison = Long.compare(a.getParentBreadth(), b.getParentBreadth());
44+
if (parentBreadthComparison != 0) {
45+
return parentBreadthComparison;
5446
}
5547
// Investigate lower breadth index first (to respect ValueSortingManner)
5648
return Long.compare(b.getBreadth(), a.getBreadth());

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/OriginalOrderNodeComparator.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,9 @@ public class OriginalOrderNodeComparator<Solution_> implements Comparator<Exhaus
1212
@Override
1313
public int compare(ExhaustiveSearchNode<Solution_> a, ExhaustiveSearchNode<Solution_> b) {
1414
// Investigate deeper first
15-
int aDepth = a.getDepth();
16-
int bDepth = b.getDepth();
17-
if (aDepth < bDepth) {
18-
return -1;
19-
} else if (aDepth > bDepth) {
20-
return 1;
15+
var depthComparison = Integer.compare(a.getDepth(), b.getDepth());
16+
if (depthComparison != 0) {
17+
return depthComparison;
2118
}
2219
// Investigate lower breadth index first (to respect ValueSortingManner)
2320
return Long.compare(b.getBreadth(), a.getBreadth());

0 commit comments

Comments
 (0)