4

Something that's been driving me crazy with python... I used to think it was just Windows, but I was wrong. I can have the same exact code and run it multiple times and it executes in wildly different amounts of time. Take the following test code, for example:

import math
def fib(count):
    x = 0
    while x < count:
        a = int(((((1 + math.sqrt(5)) / 2) ** x) - (((1 - math.sqrt(5)) / 2) ** (x))) / math.sqrt(5))
        x+=1

if __name__ == '__main__':  
    import timeit

    t = timeit.Timer("fib(1250)", setup="from __main__ import fib",)
    #print t.timeit(10)
    count = 10000
    results = t.repeat(count, 1)
    min = 0xFFFF
    max = 0
    sum = 0
    for i in results:
        i = i*1000.0
        if i < min: min = i
        if i > max: max = i
        sum+=i

    print "Min {:.3f} | Max {:.3f} | Max/Min {:.3f} | Avg {:.3f}".format(min, max, max/min, sum/count)

Basically, it generates the first 1250 elements of fibonacii 10,000 times and uses timeit to get the amount of time each run takes. I then coalesce those times and find min, max, average and variance between min and max (the spread, if you will).

Here's the results:

Windows: Min 3.071 | Max 8.903 | Max/Min 2.899 | Avg 3.228
Mac OS:  Min 1.531 | Max 3.167 | Max/Min 2.068 | Avg 1.621
Ubuntu:  Min 1.242 | Max 10.090 | Max/Min 8.123 | Avg 1.349

So, Linux is the fastest but also has the most variance. By a LOT. But all of them can have a pretty wild swing: Only 200% for Mac, but 290% for Windows and 810% for linux!

Is it actually taking that much different time to execute? Is timeit not accurate enough? Is there something else I am missing? I'm working a lot with generating animations and I need as consistent time as possible.

Community
  • 1
  • 1
Adam Haile
  • 30,705
  • 58
  • 191
  • 286
  • Depending on the timer and your implementation, measured times can be affected by other processes running on your system. This is especially true for wall clock timers. – ScottO Jul 15 '14 at 15:28
  • See... that's what I was thinking (that the actual execution time is pretty constant) but the measured times are pretty consistent in how much they vary. – Adam Haile Jul 15 '14 at 15:29
  • You are multiplying the times by 1000. Do this instead by repeating the code so that the one test runs close to 1 second. – Antti Haapala -- Слава Україні Jul 15 '14 at 15:34
  • @Adam Haile - Might want to check this out: http://stackoverflow.com/questions/15176619/timing-the-cpu-time-of-a-python-program – ScottO Jul 15 '14 at 15:34
  • @AnttiHaapala - I don't think I get what you mean... – Adam Haile Jul 15 '14 at 15:42
  • Side note... I've been running much of my python code on a console only Raspberry Pi where not much else is going on and the timings are VERY... could it be that testing this on a full UI OS is causing these weird timings? Granted, in generally I'm dissapointed by the python speed :( – Adam Haile Jul 15 '14 at 15:44
  • 1
    @AdamHaile: Python may or may not be very slow, it really depends on your code and libraries. If you are calculating things, using NumPy may make a huge difference. Python as such is not good for numerical heavy lifting. On the other hand, Python is very good with complicated data structures (dicts, sets, lists). – DrV Jul 15 '14 at 15:46
  • I'm thought about trying NumPy but I've never found a way to make it apply specifically to the calculations I'm doing... maybe I just need to look harder ;) – Adam Haile Jul 15 '14 at 15:51

2 Answers2

10

You are measuring very short times, and then even a little bit of something happening somewhere has a big impact.

I ran your test script on my machine (OS X, Core i7, Python 2.7) and made this plot of results:

enter image description here

You can see that most of the time the timing results are very consistent, but there are isolated incidents of the algorithm taking much more time (because there is something else happening).


I made a tiny adjustment to your timing procedure:

results=t.repeat(10, 1000)

So, now we are timing runs of 1000 function calls. The total amount of time is the same, naturally (10000 calls):

enter image description here

Now you can see that the performance is much more predictable. It may be that part of your wobbly timings are due to the timing methodology, not due really different times to carry out anything. Millisecond-level timing is difficult in a real-world OS environment. Even when your computer is "doing nothing", it is still switching tasks, doing background jobs, etc.


I understand the original point was not to calculate the Fibonacci numbers. But if it were, then choosing the right tool makes a difference:

import numpy as np

def fib(count):
    x = np.arange(count)
    a = (((1 + np.sqrt(5))/2) ** x - ((1 - np.sqrt(5)) / 2) ** x) / np.sqrt(5)
    a = a.astype('int')

This gives:

Min 0.120 | Max 0.471 | Max/Min 3.928 | Avg 0.125

Ten-fold speed improvement.


About the images in this answer, they are plotted with matplotlib. The first one is done thus:

import matplotlib.pyplot as plt

# create a figure
fig = plt.figure()
# create axes into the figure
ax = fig.add_subplot(111)
# plot the vector results with dots of size 2 (points) and semi-transparent blue color
ax.plot(results, '.', c=(0, 0, 1, .5), markersize=2)

See the documentation of matplotlib. It is easiest to get started by using IPython and pylab.

DrV
  • 22,637
  • 7
  • 60
  • 72
  • Nice graph... how'd you make that? So, hmmm... maybe I'm right that the reason I'm used to the timing being consistent was that I was running on a Raspberry Pi with no UI and almost no services running. My python was getting 90%+ of the CPU... – Adam Haile Jul 15 '14 at 15:46
  • fair enough... I kind of figured that would be the case and that timing only 1 run at a time was not the best way. Problem I'm running into is that I'm trying to generate frames for animations and provide a consistent framerate. So any variance in a single frame step can make it jerky. The only way around that seems to limit the frame time to a time greater than the maximum generation time. It's smooth, but slower. – Adam Haile Jul 15 '14 at 16:04
  • @AdamHaile: If you look at the percentual variation with longer runs, it is small. It is easy to get single-digit millisecond delays, but already a ten-millisecond delay is a long one. So it may be that you will not end up in trouble with your animation, after all. However, if you can calculate your animation a bit beforehand, you might consider having a buffer of a few frames. - And when you have your animation code running, clock the real wall-clock timestamps for the start and end of calculation for each frame. Then you'll see if there are OS-related extra delays. – DrV Jul 15 '14 at 16:11
  • `timeit.Timer` also accepts a 'number' param to quickly time a single run of X iterations. `timed_execution = timeit.Timer("fib(1250)", setup="from __main__ import fib", number=1000)` – Jonathan Vanasco Jul 15 '14 at 16:17
3

I made a plot similar to @drV (give him the vote, he was first!) - the difference is that I sorted results so you could see a trend line. The hint is that even though the max is high, the average is low, so there are some outliers at the end.

enter image description here

I used pylab by just adding this to the bottom:

from pylab import *
results.sort()
plot(range(len(results)),results)
show()
tdelaney
  • 73,364
  • 6
  • 83
  • 116
  • A definite +1 for the nice way of showing the distribution! (And maybe a -.01 for using `from pylab import *` interface :) – DrV Jul 15 '14 at 15:58