Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions solution_move_stops_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

package nextroute

import (
"slices"
)

// SolutionMoveStopsGeneratorChannel generates all possible moves for a given
// vehicle and plan unit.
//
Expand Down Expand Up @@ -108,11 +112,13 @@ func SolutionMoveStopsGenerator(
return
}

// TODO: we can reuse the stopPositions slice from m
positions := make([]StopPosition, len(source))
positions := m.(*solutionMoveStopsImpl).stopPositions[:0]
positions = slices.Grow(positions, len(source))
for idx := range source {
positions[idx].stopIndex = source[idx].index
positions[idx].solution = source[idx].solution
positions = append(positions, StopPosition{
stopIndex: source[idx].index,
solution: source[idx].solution,
})
}

locations := make([]int, 0, len(source))
Expand Down
100 changes: 54 additions & 46 deletions solution_sequence_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package nextroute
import (
"math/rand"
"slices"
"sync/atomic"
)

// SequenceGeneratorChannel generates all possible sequences of solution stops
Expand All @@ -29,54 +28,55 @@ func SequenceGeneratorChannel(
pu SolutionPlanUnit,
quit <-chan struct{},
) chan SolutionStops {
planUnit := pu.(*solutionPlanStopsUnitImpl)
solution := planUnit.solution()
maxSequences := int64(solution.Model().SequenceSampleSize())
solutionStops := planUnit.SolutionStops()
ch := make(chan SolutionStops)
go func() {
defer close(ch)
switch planUnit.ModelPlanStopsUnit().NumberOfStops() {
case 1:
ch <- solutionStops
return
default:
used := make([]bool, len(solutionStops))
inDegree := map[int]int{}
modelPlanUnit := planUnit.ModelPlanUnit().(*planMultipleStopsImpl)
dag := modelPlanUnit.dag.(*directedAcyclicGraphImpl)
for _, solutionStop := range solutionStops {
inDegree[solutionStop.ModelStop().Index()] = 0
}
for _, arc := range dag.arcs {
inDegree[arc.Destination().Index()]++
sequenceGeneratorSync(pu, func(solutionStops SolutionStops) {
select {
case <-quit:
return
case ch <- slices.Clone(solutionStops):
}

sequenceGenerator(
solutionStops,
make([]SolutionStop, 0, len(solutionStops)),
used,
inDegree,
dag,
solution.Random(),
&maxSequences,
func(solutionStops SolutionStops) {
select {
case <-quit:
return
case ch <- solutionStops:
}
},
-1,
)
}
})
}()

return ch
}

func sequenceGenerator(
stops, sequence SolutionStops,
func sequenceGeneratorSync(pu SolutionPlanUnit, yield func(SolutionStops)) {
planUnit := pu.(*solutionPlanStopsUnitImpl)
solutionStops := planUnit.solutionStops
if planUnit.ModelPlanStopsUnit().NumberOfStops() == 1 {
yield(planUnit.SolutionStops())
return
}
solution := planUnit.solution()
maxSequences := int64(solution.Model().SequenceSampleSize())
nSolutionStops := len(solutionStops)
used := make([]bool, nSolutionStops)
inDegree := make(map[int]int, nSolutionStops)
modelPlanUnit := planUnit.ModelPlanUnit().(*planMultipleStopsImpl)
dag := modelPlanUnit.dag.(*directedAcyclicGraphImpl)
for _, arc := range dag.arcs {
inDegree[arc.Destination().Index()]++
}

recursiveSequenceGenerator(
solutionStops,
make([]SolutionStop, 0, nSolutionStops),
used,
inDegree,
dag,
solution.Random(),
&maxSequences,
yield,
-1,
)
}

func recursiveSequenceGenerator(
stops []SolutionStop,
sequence SolutionStops,
used []bool,
inDegree map[int]int,
dag DirectedAcyclicGraph,
Expand All @@ -85,21 +85,27 @@ func sequenceGenerator(
yield func(SolutionStops),
directSuccessor int,
) {
if len(sequence) == len(stops) {
if atomic.AddInt64(maxSequences, -1) >= 0 {
yield(slices.Clone(sequence))
if *maxSequences == 0 {
return
}
nStops := len(stops)
if len(sequence) == nStops {
*maxSequences--
if *maxSequences >= 0 {
yield(sequence)
}
return
}

stopOrder := random.Perm(len(stops))
stopOrder := random.Perm(nStops)

// we know the direct successor, so we move it to the front of the random
// sequence
if directSuccessor != -1 {
for _, stopIdx := range stopOrder {
if stops[stopIdx].Index() == directSuccessor {
stopOrder = []int{stopIdx}
stopOrder = stopOrder[:1]
stopOrder[0] = stopIdx
Comment on lines +107 to +108
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It kind of makes sense that this is faster (I assume). I wonder whether it would make sense to introduce a helper func to make the intention more obvious. Something like:

func recycleSlice(s *[]int, n int) {
	*s = (*s)[:n]
}

Then again, this may defeat the purpose. 😅
Adding comments all over the place like this is probably also not great:

// we recycle the slice instead of creating a new one for performance reasons
stopOrder = stopOrder[:1]
stopOrder[0] = stopIdx

I suppose in a performance driven code-base it's fine like it is. What do you think @dirkschumacher ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI: a quick benchmark on the func approach yields this to me, but double check me as I am a bit side-tracked right now:

goos: linux
goarch: amd64
pkg: xxx
cpu: AMD Ryzen 7 5800H with Radeon Graphics         
BenchmarkRecycleInline-16      	1000000000	         1.877 ns/op	       8 B/op	       0 allocs/op
BenchmarkRecycleFunction-16    	1000000000	         1.164 ns/op	       8 B/op	       0 allocs/op
BenchmarkRecycleNew-16         	91827982	        12.27 ns/op	      16 B/op	       1 allocs/op
PASS

Based on this code:

package recycle_slice_test

import "testing"

func recycleSlice(s *[]int, n int) {
	*s = (*s)[:n]
}

func BenchmarkRecycleInline(b *testing.B) {
	slice := make([]int, b.N)
	value := 20
	for i := 0; i < b.N; i++ {
		slice = slice[:1]
		slice[0] = value
	}
}

func BenchmarkRecycleFunction(b *testing.B) {
	slice := make([]int, b.N)
	value := 20
	for i := 0; i < b.N; i++ {
		recycleSlice(&slice, 1)
		slice[0] = value
	}
}

func BenchmarkRecycleNew(b *testing.B) {
	slice := make([]int, b.N)
	value := 20
	for i := 0; i < b.N; i++ {
		slice = make([]int, 1)
		slice[0] = value
	}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally find

stopOrder = stopOrder[:1]
stopOrder[0] = stopIdx

relatively clear without a comment.

break
}
}
Expand Down Expand Up @@ -128,7 +134,9 @@ func sequenceGenerator(
}
}
}
sequenceGenerator(stops, append(sequence, stop), used, inDegree, dag, random, maxSequences, yield, directSuccessor)
recursiveSequenceGenerator(
stops, append(sequence, stop), used, inDegree, dag, random, maxSequences, yield, directSuccessor,
)
// reached the maximum number of sequences
if *maxSequences == 0 {
return
Expand Down
6 changes: 2 additions & 4 deletions solution_vehicle.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,12 +316,10 @@ func (v SolutionVehicle) bestMovePlanMultipleStops(
preAllocatedMoveContainer *PreAllocatedMoveContainer,
) SolutionMove {
var bestMove SolutionMove = newNotExecutableSolutionMoveStops(planUnit)
quitSequenceGenerator := make(chan struct{})
defer close(quitSequenceGenerator)
for sequence := range SequenceGeneratorChannel(planUnit, quitSequenceGenerator) {
sequenceGeneratorSync(planUnit, func(sequence SolutionStops) {
newMove := v.bestMoveSequence(ctx, planUnit, sequence, preAllocatedMoveContainer)
bestMove = takeBestInPlace(bestMove, newMove)
}
})
return bestMove
}

Expand Down
Loading