Search code examples
pythonpython-3.xoptimizationtiming

Python integer comparison (not equal to 0) benchmarks


I was making a 2D physics engine in python for fun. I use an object's velocity to determine if I should run the collision detection on it. I.e if it's velocity would cause it to be inside another object on the next cycle, then I run the collision detection on it.

Note: x and y velocities are always integers

So I realised that I would have to check that the object's x or y velocities are either less than or greater than (not) 0.

There are a lot of ways to do this. I came up with 4 different methods:

def cmp1 (a,b):
    return a > 0 or a < 0 or b > 0 or b < 0

def cmp2 (a, b):
    return a != 0 or b != 0

def cmp3 (a, b):
    return abs(a | b) > 0

def cmp4 (a, b):
    return a | b != 0

I like the 4th method because it has the fewest operations and reads well in English a or b does not equal 0

But of the 4 methods, and of the benchmarking method I used, it has the 2nd slowest average time which surprised me. In fact, the first method is the fastest (again, using the benchmarking method I used).

Here is the method I used to work out the timings:

def randints(min, max, amount):
    return tuple([random.randint(min, max) for i in range(amount)])

times = []
for i in range(1000):
    n = randints(-9999, 9999, 2)
    t = time.time() * 1000
    cmp4(*n)
    t2 = time.time() * 1000
    times.append(t2-t)

print('Average Time: ', sum(times)/len(times))

If I run this multiple times for each method, I get very consistent results:

cmp1 - 0.00027s

cmp2 - 0.00032s

cmp3 - 0.00037s

cmp4 - 0.00035s

So my question is why is the 4th method so slow compared to the first? surely 2 operations (one bitwise, one comparison) should be faster than 4.

And, yes, I know its a difference of only a couple of microseconds.


Solution

  • There are better testing methods

    y = [(random.randint(0,1), random.randint(0,1)) for _ in range(100_000)]
    
    def test(cmp):
        [cmp(a, b) for a, b in y]
    
    %timeit test(cmp1)
    20.1 ms ± 315 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
    %timeit test(cmp2)
    17.3 ms ± 212 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    %timeit test(cmp3)
    29.3 ms ± 588 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
    %timeit test(cmp4)
    21.9 ms ± 289 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
    

    The main advantage of or is that it is short circuiting, effectively equivalent to:

    def or(a, b):
        if a: return a
        return b
    

    Whereas | requires an actual (very low level) operation to be performed, then a new python integer object to be created and then another operation to be performed.

    My test uses an array which is 50% zeros. If there was a larger or smaller percentage of of zeros the results may be quite different.

    In any case, you forgot the fastest method of all (since all non-zero numbers are True):

    def cmp0(a, b):
        return a or b
    
    bool(cmp0(0, 0)), bool(cmp0(-12, 0)), bool(cmp0(3.14, 0))
    # --> (False, True, True)
    
    %timeit test(cmp0)
    12.8 ms ± 191 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    

    However if you wanted to be as clear as possible (something at the heart of programming in python), I'd use a != 0 or b != 0.