mikepound / mazesolving

A variety of algorithms to solve mazes from an input image
The Unlicense
1.74k stars 410 forks source link

Use a faster priority queue implementation #8

Closed jmitchell closed 7 years ago

jmitchell commented 7 years ago

A priority queue based on Python's heapq is significantly faster than FibHeap. The PriorityQueue abstract class establishes a standard interface used by both implementations, and minimal changes were required to astar.py and dijkstra.py to use it.

Performance analysis

FibPQ

fibpq

$ time python ./profile.py
Loading Image
Creating Maze
('Node Count:', 716516)
('Time elapsed:', 7.4103100299835205, '\n')
('Starting Solve:', 'A-star Search')
('Nodes explored: ', 462307)
('Path found, length', 8650)
('Time elapsed: ', 32.061322927474976, '\n')
Saving Image
Loading Image
Creating Maze
('Node Count:', 716516)
('Time elapsed:', 8.040678024291992, '\n')
('Starting Solve:', 'Breadth first search')
('Nodes explored: ', 512576)
('Path found, length', 8650)
('Time elapsed: ', 2.32486891746521, '\n')
Saving Image
Loading Image
Creating Maze
('Node Count:', 716516)
('Time elapsed:', 8.369711875915527, '\n')
('Starting Solve:', 'Depth first search')
('Nodes explored: ', 152711)
('Path found, length', 8650)
('Time elapsed: ', 0.6843249797821045, '\n')
Saving Image
Loading Image
Creating Maze
('Node Count:', 716516)
('Time elapsed:', 7.939746856689453, '\n')
('Starting Solve:', "Dijkstra's Algorithm")
('Nodes explored: ', 511299)
('Path found, length', 8650)
('Time elapsed: ', 39.57828378677368, '\n')
Saving Image
Loading Image
Creating Maze
('Node Count:', 716516)
('Time elapsed:', 9.189558982849121, '\n')
('Starting Solve:', 'Left turn only')
('Nodes explored: ', 582092)
('Path found, length', 582092)
('Time elapsed: ', 1.0989949703216553, '\n')
Saving Image
python profile.py  127.03s user 1.56s system 100% cpu 2:08.42 total

HeapPQ

heappq

$ time python ./profile.py
Loading Image
Creating Maze
('Node Count:', 716516)
('Time elapsed:', 7.687443017959595, '\n')
('Starting Solve:', 'A-star Search')
('Nodes explored: ', 462306)
('Path found, length', 8650)
('Time elapsed: ', 9.670147895812988, '\n')
Saving Image
Loading Image
Creating Maze
('Node Count:', 716516)
('Time elapsed:', 8.07496190071106, '\n')
('Starting Solve:', 'Breadth first search')
('Nodes explored: ', 512576)
('Path found, length', 8650)
('Time elapsed: ', 2.324921131134033, '\n')
Saving Image
Loading Image
Creating Maze
('Node Count:', 716516)
('Time elapsed:', 8.762192964553833, '\n')
('Starting Solve:', 'Depth first search')
('Nodes explored: ', 152711)
('Path found, length', 8650)
('Time elapsed: ', 0.719641923904419, '\n')
Saving Image
Loading Image
Creating Maze
('Node Count:', 716516)
('Time elapsed:', 8.0798499584198, '\n')
('Starting Solve:', "Dijkstra's Algorithm")
('Nodes explored: ', 511299)
('Path found, length', 8650)
('Time elapsed: ', 12.476689100265503, '\n')
Saving Image
Loading Image
Creating Maze
('Node Count:', 716516)
('Time elapsed:', 8.351932048797607, '\n')
('Starting Solve:', 'Left turn only')
('Nodes explored: ', 582092)
('Path found, length', 582092)
('Time elapsed: ', 0.9704020023345947, '\n')
Saving Image
python profile.py  76.46s user 1.72s system 100% cpu 1:17.91 total
jmitchell commented 7 years ago

While testing more mazes I found a minor bug with the new priority queue. Please wait to merge until I've submitted a fix.

mikepound commented 7 years ago

This is interesting. I ignored the HeapPQ because I thought didn't didn't have a "decreasekey" function. It seems you've implemented one through a remove + add option. It certainly looks faster, wikipedia suggests that a fibheap is the optimal pq structure, but I strongly suspect that depends on the way it's being used, and also relies on my implementation being optimised, which it probably isn't!

jmitchell commented 7 years ago

I fixed the bug and profiled more extensively on other inputs. The initial gains I saw on perfect2k.png apparently don't generalize. There's more context in the latest commit comment. I anticipate more refactoring will lead to performance improvements.

Fibonacci heaps, as I understand it, are theoretically ideal, but not necessarily in practice. Binary heaps, like heapq's implementation, can represent the tree in a contiguous array of memory, which is better for cache locality. In contrast Fibonacci heaps are less consistently structured, so they necessarily have more layers of indirection. I don't know in this case which approach will prove fastest after optimizations, but I'm really curious!

jmitchell commented 7 years ago

The latest change is a nice speed improvement for all priority queues. It includes changing to the HeapPQ implementation. More info in commit message, and latest profiler images below.

FibPQ

profile.py 203.06s user 3.63s system 100% cpu 3:26.50 total

fibpq

HeapPQ

profile.py 130.79s user 2.84s system 100% cpu 2:13.50 total

heappq

mikepound commented 7 years ago

Also, having thought about it, even if the fib heap decreasekey is very efficient, I'm not actually sure it's called on a perfect maze. There's never an alternative path to a node, so we're never in a situation where it's needed. It's pretty rare even in the braid mazes. A pq that focuses on speed of insert and removemin is likely to be faster.