Right now my ConstraintProvider is structured as follows:
public class SchedulingConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory cf) {
return new Constraint[] {
teacherBreakConstraint(cf),
teacherConflictConstraint(cf)
};
}
// Simply for code reuse. Method called from two places.
UniConstraintStream<Lecture> scheduledLecturesWithTeachers(ConstraintFactory cf) {
return cf.forEach(Lecture.class)
.filter(Lecture::isScheduled)
.filter(Lecture::hasTeacher);
}
private Constraint teacherBreakConstraint(ConstraintFactory cf) {
return scheduledLecturesWithTeachers(cf) // <---- called here...
.join(scheduledLecturesWithTeachers(cf),
Joiners.equal(Lectures::getTeacher),
...some filtering)
...
.penalize(ONE_HARD)
.asConstraint("teacher-break");
}
private Constraint teacherConflictConstraint(ConstraintFactory cf) {
return scheduledLecturesWithTeachers(cf) // <---- ...and here
.join(scheduledLecturesWithTeachers(cf),
Joiners.equal(Lectures::getTeacher),
... some other filtering)
...
.penalize(ONE_HARD)
.asConstraint("teacher-conflict");
}
}
Would it improve OptaPlanners performance if I restructured my code so that the scheduledLecturesWithTeachers constraint stream was created once and shared between the two constraints?
@Override
public Constraint[] defineConstraints(ConstraintFactory cf) {
// Create this common part ONCE, then reuse the same object in both constraints...
UniConstraintStream<Lecture> scheduledLecturesWithTeachers = scheduledLecturesWithTeachers(cf);
return new Constraint[] {
teacherBreakConstraint(scheduledLecturesWithTeachers, cf),
teacherConflictConstraint(scheduledLecturesWithTeachers, cf)
};
}
UniConstraintStream<Lecture> scheduledLecturesWithTeachers(ConstraintFactory cf) {
return cf.forEach(Lecture.class)
.filter(Lecture::isScheduled)
.filter(Lecture::hasTeacher);
}
private Constraint teacherBreakConstraint(UniConstraintStream<Lecture> scheduledLecturesWithTeachers, ConstraintFactory cf) {
return scheduledLecturesWithTeachers // <---- used here...
.join(scheduledLecturesWithTeachers(cf),
Joiners.equal(Lectures::getTeacher),
...some filtering)
...
.penalize(ONE_HARD)
.asConstraint("teacher-break");
}
private Constraint teacherConflictConstraint(UniConstraintStream<Lecture> scheduledLecturesWithTeachers, ConstraintFactory cf) {
return scheduledLecturesWithTeachers // <---- ...and here
.join(scheduledLecturesWithTeachers(cf),
Joiners.equal(Lectures::getTeacher),
... some other filtering)
...
.penalize(ONE_HARD)
.asConstraint("teacher-conflict");
}
Taking this one step further, would it be useful to even reuse it twice in the same constraint?
private Constraint teacherConflictConstraint(UniConstraintStream<Lecture> scheduledLecturesWithTeachers, ConstraintFactory cf) {
return scheduledLecturesWithTeachers // <---- reused object
.join(scheduledLecturesWithTeachers, // <---- reused again in same constraint
Joiners.equal(Lectures::getTeacher),
... some other filtering)
...
.penalize(ONE_HARD)
.asConstraint("teacher-conflict");
}
>Solution :
Yes and no. What you are trying to do is called "node sharing", and it is a proven performance optimization technique.
In the case of DROOLS, the default implementation of Constraint Streams, node sharing is unlikely to bring any performance improvements. Drools itself does support node sharing, but our CS implementation on top makes some abstractions that often prevent Drools node sharing.
In the case of BAVET, the alternative implementation of Constraint Streams, this most certainly does bring performance benefits. Every stream instance you reuse only gets executed once, no matter how many times you reuse it.
In general, we discourage this approach, because it makes the code read worse and harder to understand. That said, if ultimate performance is the goal, then node sharing is the way to get there.