Recently I am working on a tower-defense game in python (3.7). In the code of this game I have to check the distance between different 2-d points alot. So I wanted to check what the fastest methods for completing this task are.
I am not firm about the particular norm. The 2-norm seems like natural choice, but im not oppossed to change to the infinity-norm, if neccessary for execution time (https://en.wikipedia.org/wiki/Lp_space).
I was under the impression, that list-comprehension is faster than for loops and also build in functions (e.g. in numpy) are faster than most of code I could write. I tested some alternatives for computing the distance and was suprised that I can not confirm this impression. To my suprise the numpy-linalg.norm-function performs horrible. This was the code I used to test runtimes:
import timeit
import numpy as np
import test_extension
number_of_repeats = 10000
tower_pos = (3,5)
enemy_pos = ((1,2),(2,3),(3,4),(4,5),(5,6),(6,7),(7,8),(8,9))
distance = 3
options = ('np.linalg 2 norm', 'np.linalg inf norm', 'inf norm manuel', 'manuel squared 2 norm', 'hard coded squared 2 norm', 'c-extension hard coded squared 2 norm', 'all in extension')
def list_comprehension(option):
if option == 0:
return [pos for pos in enemy_pos if np.linalg.norm(np.subtract(tower_pos, pos)) <= distance]
elif option == 1:
return [pos for pos in enemy_pos if np.linalg.norm(np.subtract(tower_pos, pos), ord = 1) <= distance]
elif option == 2:
return [pos for pos in enemy_pos if max(abs(np.subtract(tower_pos, pos))) <= distance]
elif option == 3:
return [pos for pos in enemy_pos if sum(np.square(np.subtract(tower_pos, pos))) <= distance**2]
elif option == 4:
return [pos for pos in enemy_pos for distance_vector in [np.subtract(tower_pos, pos)] if distance_vector[0]*distance_vector[0]+distance_vector[1]*distance_vector[1] <= distance**2 ]
elif option == 5:
return [pos for pos in enemy_pos if test_extension.distance(np.subtract(tower_pos, pos)) <= distance**2]
elif option == 6:
return test_extension.list_comprehension(tower_pos, enemy_pos, distance)
def for_loop(option):
l = []
if option == 0:
for pos in enemy_pos:
distance_vector = np.subtract(tower_pos, pos)
norm = np.linalg.norm(distance_vector)
if norm <= distance:
l.append(pos)
elif option == 1:
for pos in enemy_pos:
distance_vector = np.subtract(tower_pos, pos)
norm = np.linalg.norm(distance_vector, ord = np.inf)
if norm <= distance:
l.append(pos)
elif option == 2:
for pos in enemy_pos:
distance_vector = np.subtract(tower_pos, pos)
norm = max(abs(distance_vector))
if norm <= distance:
l.append(pos)
elif option == 3:
for pos in enemy_pos:
distance_vector = np.subtract(tower_pos, pos)
norm = sum(distance_vector * distance_vector)
if norm <= distance**2:
l.append(pos)
elif option == 4:
for pos in enemy_pos:
distance_vector = np.subtract(tower_pos, pos)
norm = distance_vector[0]*distance_vector[0]+distance_vector[1]*distance_vector[1]
if norm <= distance**2:
l.append(pos)
elif option == 5:
d = test_extension.distance
for pos in enemy_pos:
distance_vector = np.subtract(tower_pos, pos)
norm = d(distance_vector)
if norm <= distance**2:
l.append(pos)
elif option == 6:
return test_extension.for_loop(tower_pos, enemy_pos, distance)
return l
print(f"{'_'.ljust(40)}list_comprehension for_loop")
for i,option in enumerate(options):
times = [timeit.timeit(f'{method}({i})', number = number_of_repeats, globals = {f'{method}': globals()[f'{method}']}) for method in ['list_comprehension', 'for_loop']]
s = f'{option}:'.ljust(40)
print(f"{s}{round(times[0],5)}{' '*15} {round(times[1],5)}")
which gave the following output:
list_comprehension for_loop
np.linalg 2 norm: 3.58053 3.56676
np.linalg inf norm: 3.17169 3.53372
inf norm manuel: 1.15261 1.1951
manuel squared 2 norm: 1.30239 1.28485
hard coded squared 2 norm: 1.11324 1.08586
c-extension hard coded squared 2 norm: 1.01506 1.05114
all in extension: 0.81358 0.81262
test_extension contains the below code. I then created a c-extension from test_extension using the package cython (https://cython.readthedocs.io/en/latest/src/tutorial/cython_tutorial.html):
import numpy as np
def distance(vector: np.array):
return vector[0]*vector[0]+vector[1]*vector[1]
def for_loop(single_pos, pos_list, distance):
l = []
for pos in pos_list:
distance_vector = np.subtract(single_pos, pos)
norm = distance_vector[0]*distance_vector[0]+distance_vector[1]*distance_vector[1]
if norm <= distance**2:
l.append(pos)
return l
def list_comprehension(single_pos, pos_list, distance):
return [pos for pos in pos_list for distance_vector in [np.subtract(single_pos, pos)] if distance_vector[0]*distance_vector[0]+distance_vector[1]*distance_vector[1] <= distance**2 ]
At the moment i mainly have the 3 below questions, but feel free to give other insights you can share:
- Why are the build in np.linalg.norms so slow? Am I missusing those?
- Why is hardcoding the squared 2 norm for a vector v better than using sum(np.square(v))?
- Have I missed even faster methods for computing the distance (as 2-norm)?