Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Surprised about good recursion performance in python

I wrote this rather poor Python function for prime factorization:

import math

def factor(n):
    for i in range(2, int(math.sqrt(n)+1)):
        if not n % i:
            return [i] + factor(n//i)
    return [n]

and it worked as expected, now I was interested in whether the performance could be better when using an iterative approach:

def factor_it(n):
    r = []
    i = 2
    while i < int(math.sqrt(n)+1):
        while not n % i:
            r.append(i)
            n //= i
        i +=1
    if n > 1:
        r.append(n)
    return r

But what I observed (while the functions gave the same results) was that the iterative function took longer to run. At least I had the feeling, when doing this:

number = 31123478114123
print(factor(number))
print(factor_it(number))

so I measured:

setup = '''
import math

def factor(n):
    for i in range(2, int(math.sqrt(n)+1)):
        if not n % i:
            return [i] + factor(n//i)
    return [n]

def factor_it(n):
    r = []
    i = 2
    while i < int(math.sqrt(n)+1):
        while not n % i:
            r.append(i)
            n //= i
        i +=1
    if n > 1:
        r.append(n)
    return r
'''

import timeit

exec(setup)

number = 66666667*952381*290201
print(factor(number))
print(factor_it(number))

print(timeit.Timer('factor('+str(number)+')',setup=setup).repeat(1,1))
print(timeit.Timer('factor_it('+str(number)+')',setup=setup).repeat(1,1))

And this is what I got:

[290201, 952381, 66666667]
[290201, 952381, 66666667]
[0.19888348945642065]
[0.7451271022307537]

Why is the recursive approach in this case faster than the iterative one?

I use WinPython-64bit-3.4.4.2 (Python 3.4.4 64bits).

like image 894
Wolf Avatar asked Aug 05 '16 08:08

Wolf


1 Answers

It’s because you’re recomputing sqrt each time. This modification runs as fast as your recursive version:

def factor_it2(n):
    r = []
    i = 2
    lim = int(math.sqrt(n)+1)
    while i < lim:
        while not n % i:
            r.append(i)
            n //= i
        lim = int(math.sqrt(n)+1)
        i += 1
    if n > 1:
        r.append(n)
    return r

timeit gives me these times:

factor      0.13133816363922143
factor_it   0.5705408816539869
factor_it2  0.14267319543853973

I think the tiny difference that remains is due to for … in range(…) being faster than the equivalent while loop, as the for loop can use a generator instead of having to execute a bunch of comparisons.

like image 173
Lynn Avatar answered Sep 21 '22 06:09

Lynn