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)?
 
     
    