I have an A* planning algorithm written purely in Python (using this excellent resource on path planning). The grid class with associated methods for movement and costs is defined as follows:
class SquareGrid:
def __init__(self, width, height):
self.width = width
self.height = height
self.walls = []
self.weights = []
def in_bounds(self, id):
(x, y) = id
return 0 <= x < self.width and 0 <= y < self.height
def passable(self, id):
return id not in self.walls
def neighbors(self, id):
(x, y) = id
results = [(x + 1, y), (x + 1, y - 1), (x, y - 1), (x - 1, y - 1), (x - 1, y), (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)]
if (x + y) % 2 == 0: results.reverse() # aesthetics
results = [r for r in results if self.in_bounds(r)]
results = [r for r in results if self.passable(r)] # this is where things slow down
return results
def cost(self, from_node, to_node):
return (to_node[0] - from_node[0])**2 + (to_node[1] - from_node[1])**2
Now, I'd like to speed up execution a bit using Cython's static compiler goodness. One option would have been to rewrite the whole thing with static typing in Cython. I profiled the pure Python code using cProfiler to see where the bottleneck lies, and unsurprisingly enough, about 70% of the total execution time went into the neighbors
method (which computes the valid neighboring nodes around the current node). More specifically, the list comprehension line in neighbors
calls passable
more than 33,000 times for a given toy example. passable
checks if the node given in its argument is marked as "obstacle" or not by searching through SquareGrid
's knowledge of obstacles (SquareGrid.walls
, a list of location tuples) and returns a boolean accordingly. It seemed to me that just by optimizing this particular method, I'd get significant gains in speed. So I set about rewriting passable
in Cython.
I'm a complete noob at Cython and C/C++ in general, so I'd appreciate if any mistake in understanding how this thing actually works is pointed out. I created a pyrex file passable_cy.pyx
with the hope that compiling it using Cython/GCC++ and then binding it to a SquareGrid
object in the main script would be enough. This is how I defined passable_cy.pyx
:
cpdef bint passable(object self, tuple id):
return id not in self.walls
The accompanying setup.py
file:
from distutils.core import setup
from Cython.Build import cythonize
setup(name="cynthonized_passable", ext_modules=cythonize(['passable_cy.pyx']),)
And this is how I bound the new cynthonized method to SquareGrid
in the main A* python script:
g = SquareGrid(100, 100) # create grid with dimensions 100x100
g.passable = types.MethodType(passable_cy.passable, g)
Everything compiled properly and the whole thing executed without problems. There was no speed improvement whatsoever, which I kind of expected (seemed too straightforward). How do I proceed from here? Is this method-binding thing the best way to go about it? I'm sure more can be done in passable_cy.pyx
, but I'm too unfamiliar with C/C++ to know what to do.