I wrote this solution to Project Euler #5.
import time
start_time = time.time()
def ProjectEulerFive (m = 20):
a = m
start = 2
while (m % start) == 0:
start += 1
b = start
while b < m:
if ( a % b) != 0:
a += m
b = start
continue
else:
b += 1
return a
import sys
if (len(sys.argv)) > 2:
print "error: this function takes a max of 1 argument"
elif (len(sys.argv)) == 2:
print ProjectEulerFive(int(sys.argv[1]))
else:
print ProjectEulerFive();
print "took " + str(time.time() - start_time ) + " seconds"
Takes my system about 8.5 seconds.
Then I decided to compare with other peoples solutions. I found this
Project Euler 5 in Python – How can I optimize my solution?.
I hadn’t thought of unique prime factorization.
But anyways, one supposedly optimized non-prime factorization based solution posted there:
import time
start_time = time.time()
check_list = [11, 13, 14, 16, 17, 18, 19, 20]
def find_solution(step):
for num in xrange(step, 999999999, step):
if all(num % n == 0 for n in check_list):
return num
return None
if __name__ == '__main__':
solution = find_solution(20)
if solution is None:
print "No answer found"
else:
print "found an answer:", solution
print "took " + str(time.time() - start_time ) + " seconds"
Takes my system about 37 seconds
My code is about 4 times faster even though I unnecessarily check for divisibility of 3,4,5,6,7,8,9,10, and 12.
I’m new to python, and having trouble seeing where the inefficiency is coming from.
EDIT:
I did another test.
import time
start_time = time.time()
def ProjectEulerFive (m = 20):
ls = [11, 13, 14, 15, 16, 17, 18, 19]
a = m
i = 0
while i < len(ls):
if ( a % ls[i]) != 0:
a += m
i = 0
continue
else:
i += 1
return a
print ProjectEulerFive();
print "took " + str(time.time() - start_time ) + " seconds"
Takes my system 6 seconds, but this:
import time
start_time = time.time()
def ProjectEulerFive (m = 20):
a = m
start = 11
b = start
while b < m:
if ( a % b) != 0:
a += m
b = start
continue
else:
b += 1
return a
print ProjectEulerFive()
print "took " + str(time.time() - start_time ) + " seconds"
Takes about 3.7 seconds
I see that although a faster solution has been posted, no one has actually answered the question. It’s a rather difficult one to answer, in fact! The fundamental explanation is that function calls are relatively expensive. In order to make this conclusion persuasive, though, I’ll have to dig fairly deeply into Python internals. Prepare yourself!
First of all, I’m going to disassemble (your third version of)
ProjectEulerFiveandfind_solutionfrom the “optimized” solution, usingdis.dis. There’s a lot here, but a quick scan is all that’s required to confirm that your code calls no functions at all:Now let’s look at
find_solution:Immediately it becomes clear that (a) this code is much less complex, but (b) it also calls three different functions. The first one is simply a single call to
xrange, but the other two calls appear inside the outermost for loop. The first call is the call toall; the second, I suspect, is the generator expression’snextmethod being called. But it doesn’t really matter what the functions are; what matters is that they are called inside the loop.Now, you might think “What’s the big deal?” here. It’s just a function call; a few nanoseconds here or there — right? But in fact, those nanoseconds add up. Since the outermost loop proceeds through a total of
232792560 / 20 == 11639628loops, any overhead gets multiplied by more than eleven million. A quick timing using the%timeitcommand inipythonshows that a function call — all by itself — costs about 120 nanoseconds on my machine:So for every function call that appears inside the while loop, that’s
120 nanoseconds * 11000000 = 1.32 secondsmore time spent. And if I’m right that the second function call is a call to the generator expression’snextmethod, then that function is called even more times, once for every iteration through the genex — probably 3-4 times per loop on average.Now to test this hypothesis. If function calls are the problem, then eliminating function calls is the solution. Let’s see…
Here’s a version of
find_solutionthat does almost exactly what the original does usingfor/elsesyntax. The only function call is the outer one, toxrange, which shouldn’t cause any problems. Now, when I timed the original version, it took 11 seconds:Let’s see what this new, improved version manages:
That’s a hair faster than the performance of your fastest version of
ProjectEulerFiveon my machine:And everything makes sense again.