You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
344 lines
12 KiB
344 lines
12 KiB
5 years ago
|
#! /usr/bin/env python
|
||
|
|
||
|
"""Tool for measuring execution time of small code snippets.
|
||
|
|
||
|
This module avoids a number of common traps for measuring execution
|
||
|
times. See also Tim Peters' introduction to the Algorithms chapter in
|
||
|
the Python Cookbook, published by O'Reilly.
|
||
|
|
||
|
Library usage: see the Timer class.
|
||
|
|
||
|
Command line usage:
|
||
|
python timeit.py [-n N] [-r N] [-s S] [-t] [-c] [-h] [--] [statement]
|
||
|
|
||
|
Options:
|
||
|
-n/--number N: how many times to execute 'statement' (default: see below)
|
||
|
-r/--repeat N: how many times to repeat the timer (default 3)
|
||
|
-s/--setup S: statement to be executed once initially (default 'pass')
|
||
|
-t/--time: use time.time() (default on Unix)
|
||
|
-c/--clock: use time.clock() (default on Windows)
|
||
|
-v/--verbose: print raw timing results; repeat for more digits precision
|
||
|
-h/--help: print this usage message and exit
|
||
|
--: separate options from statement, use when statement starts with -
|
||
|
statement: statement to be timed (default 'pass')
|
||
|
|
||
|
A multi-line statement may be given by specifying each line as a
|
||
|
separate argument; indented lines are possible by enclosing an
|
||
|
argument in quotes and using leading spaces. Multiple -s options are
|
||
|
treated similarly.
|
||
|
|
||
|
If -n is not given, a suitable number of loops is calculated by trying
|
||
|
successive powers of 10 until the total time is at least 0.2 seconds.
|
||
|
|
||
|
The difference in default timer function is because on Windows,
|
||
|
clock() has microsecond granularity but time()'s granularity is 1/60th
|
||
|
of a second; on Unix, clock() has 1/100th of a second granularity and
|
||
|
time() is much more precise. On either platform, the default timer
|
||
|
functions measure wall clock time, not the CPU time. This means that
|
||
|
other processes running on the same computer may interfere with the
|
||
|
timing. The best thing to do when accurate timing is necessary is to
|
||
|
repeat the timing a few times and use the best time. The -r option is
|
||
|
good for this; the default of 3 repetitions is probably enough in most
|
||
|
cases. On Unix, you can use clock() to measure CPU time.
|
||
|
|
||
|
Note: there is a certain baseline overhead associated with executing a
|
||
|
pass statement. The code here doesn't try to hide it, but you should
|
||
|
be aware of it. The baseline overhead can be measured by invoking the
|
||
|
program without arguments.
|
||
|
|
||
|
The baseline overhead differs between Python versions! Also, to
|
||
|
fairly compare older Python versions to Python 2.3, you may want to
|
||
|
use python -O for the older versions to avoid timing SET_LINENO
|
||
|
instructions.
|
||
|
"""
|
||
|
|
||
|
import gc
|
||
|
import sys
|
||
|
import time
|
||
|
try:
|
||
|
import itertools
|
||
|
except ImportError:
|
||
|
# Must be an older Python version (see timeit() below)
|
||
|
itertools = None
|
||
|
|
||
|
__all__ = ["Timer"]
|
||
|
|
||
|
dummy_src_name = "<timeit-src>"
|
||
|
default_number = 1000000
|
||
|
default_repeat = 3
|
||
|
|
||
|
if sys.platform == "win32":
|
||
|
# On Windows, the best timer is time.clock()
|
||
|
default_timer = time.clock
|
||
|
else:
|
||
|
# On most other platforms the best timer is time.time()
|
||
|
default_timer = time.time
|
||
|
|
||
|
# Don't change the indentation of the template; the reindent() calls
|
||
|
# in Timer.__init__() depend on setup being indented 4 spaces and stmt
|
||
|
# being indented 8 spaces.
|
||
|
template = """
|
||
|
def inner(_it, _timer%(init)s):
|
||
|
%(setup)s
|
||
|
_t0 = _timer()
|
||
|
for _i in _it:
|
||
|
%(stmt)s
|
||
|
_t1 = _timer()
|
||
|
return _t1 - _t0
|
||
|
"""
|
||
|
|
||
|
def reindent(src, indent):
|
||
|
"""Helper to reindent a multi-line statement."""
|
||
|
return src.replace("\n", "\n" + " "*indent)
|
||
|
|
||
|
def _template_func(setup, func):
|
||
|
"""Create a timer function. Used if the "statement" is a callable."""
|
||
|
def inner(_it, _timer, _func=func):
|
||
|
setup()
|
||
|
_t0 = _timer()
|
||
|
for _i in _it:
|
||
|
_func()
|
||
|
_t1 = _timer()
|
||
|
return _t1 - _t0
|
||
|
return inner
|
||
|
|
||
|
class Timer:
|
||
|
"""Class for timing execution speed of small code snippets.
|
||
|
|
||
|
The constructor takes a statement to be timed, an additional
|
||
|
statement used for setup, and a timer function. Both statements
|
||
|
default to 'pass'; the timer function is platform-dependent (see
|
||
|
module doc string).
|
||
|
|
||
|
To measure the execution time of the first statement, use the
|
||
|
timeit() method. The repeat() method is a convenience to call
|
||
|
timeit() multiple times and return a list of results.
|
||
|
|
||
|
The statements may contain newlines, as long as they don't contain
|
||
|
multi-line string literals.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, stmt="pass", setup="pass", timer=default_timer):
|
||
|
"""Constructor. See class doc string."""
|
||
|
self.timer = timer
|
||
|
ns = {}
|
||
|
if isinstance(stmt, basestring):
|
||
|
# Check that the code can be compiled outside a function
|
||
|
if isinstance(setup, basestring):
|
||
|
compile(setup, dummy_src_name, "exec")
|
||
|
compile(setup + '\n' + stmt, dummy_src_name, "exec")
|
||
|
else:
|
||
|
compile(stmt, dummy_src_name, "exec")
|
||
|
stmt = reindent(stmt, 8)
|
||
|
if isinstance(setup, basestring):
|
||
|
setup = reindent(setup, 4)
|
||
|
src = template % {'stmt': stmt, 'setup': setup, 'init': ''}
|
||
|
elif hasattr(setup, '__call__'):
|
||
|
src = template % {'stmt': stmt, 'setup': '_setup()',
|
||
|
'init': ', _setup=_setup'}
|
||
|
ns['_setup'] = setup
|
||
|
else:
|
||
|
raise ValueError("setup is neither a string nor callable")
|
||
|
self.src = src # Save for traceback display
|
||
|
code = compile(src, dummy_src_name, "exec")
|
||
|
exec code in globals(), ns
|
||
|
self.inner = ns["inner"]
|
||
|
elif hasattr(stmt, '__call__'):
|
||
|
self.src = None
|
||
|
if isinstance(setup, basestring):
|
||
|
_setup = setup
|
||
|
def setup():
|
||
|
exec _setup in globals(), ns
|
||
|
elif not hasattr(setup, '__call__'):
|
||
|
raise ValueError("setup is neither a string nor callable")
|
||
|
self.inner = _template_func(setup, stmt)
|
||
|
else:
|
||
|
raise ValueError("stmt is neither a string nor callable")
|
||
|
|
||
|
def print_exc(self, file=None):
|
||
|
"""Helper to print a traceback from the timed code.
|
||
|
|
||
|
Typical use:
|
||
|
|
||
|
t = Timer(...) # outside the try/except
|
||
|
try:
|
||
|
t.timeit(...) # or t.repeat(...)
|
||
|
except:
|
||
|
t.print_exc()
|
||
|
|
||
|
The advantage over the standard traceback is that source lines
|
||
|
in the compiled template will be displayed.
|
||
|
|
||
|
The optional file argument directs where the traceback is
|
||
|
sent; it defaults to sys.stderr.
|
||
|
"""
|
||
|
import linecache, traceback
|
||
|
if self.src is not None:
|
||
|
linecache.cache[dummy_src_name] = (len(self.src),
|
||
|
None,
|
||
|
self.src.split("\n"),
|
||
|
dummy_src_name)
|
||
|
# else the source is already stored somewhere else
|
||
|
|
||
|
traceback.print_exc(file=file)
|
||
|
|
||
|
def timeit(self, number=default_number):
|
||
|
"""Time 'number' executions of the main statement.
|
||
|
|
||
|
To be precise, this executes the setup statement once, and
|
||
|
then returns the time it takes to execute the main statement
|
||
|
a number of times, as a float measured in seconds. The
|
||
|
argument is the number of times through the loop, defaulting
|
||
|
to one million. The main statement, the setup statement and
|
||
|
the timer function to be used are passed to the constructor.
|
||
|
"""
|
||
|
if itertools:
|
||
|
it = itertools.repeat(None, number)
|
||
|
else:
|
||
|
it = [None] * number
|
||
|
gcold = gc.isenabled()
|
||
|
gc.disable()
|
||
|
try:
|
||
|
timing = self.inner(it, self.timer)
|
||
|
finally:
|
||
|
if gcold:
|
||
|
gc.enable()
|
||
|
return timing
|
||
|
|
||
|
def repeat(self, repeat=default_repeat, number=default_number):
|
||
|
"""Call timeit() a few times.
|
||
|
|
||
|
This is a convenience function that calls the timeit()
|
||
|
repeatedly, returning a list of results. The first argument
|
||
|
specifies how many times to call timeit(), defaulting to 3;
|
||
|
the second argument specifies the timer argument, defaulting
|
||
|
to one million.
|
||
|
|
||
|
Note: it's tempting to calculate mean and standard deviation
|
||
|
from the result vector and report these. However, this is not
|
||
|
very useful. In a typical case, the lowest value gives a
|
||
|
lower bound for how fast your machine can run the given code
|
||
|
snippet; higher values in the result vector are typically not
|
||
|
caused by variability in Python's speed, but by other
|
||
|
processes interfering with your timing accuracy. So the min()
|
||
|
of the result is probably the only number you should be
|
||
|
interested in. After that, you should look at the entire
|
||
|
vector and apply common sense rather than statistics.
|
||
|
"""
|
||
|
r = []
|
||
|
for i in range(repeat):
|
||
|
t = self.timeit(number)
|
||
|
r.append(t)
|
||
|
return r
|
||
|
|
||
|
def timeit(stmt="pass", setup="pass", timer=default_timer,
|
||
|
number=default_number):
|
||
|
"""Convenience function to create Timer object and call timeit method."""
|
||
|
return Timer(stmt, setup, timer).timeit(number)
|
||
|
|
||
|
def repeat(stmt="pass", setup="pass", timer=default_timer,
|
||
|
repeat=default_repeat, number=default_number):
|
||
|
"""Convenience function to create Timer object and call repeat method."""
|
||
|
return Timer(stmt, setup, timer).repeat(repeat, number)
|
||
|
|
||
|
def main(args=None, _wrap_timer=None):
|
||
|
"""Main program, used when run as a script.
|
||
|
|
||
|
The optional 'args' argument specifies the command line to be parsed,
|
||
|
defaulting to sys.argv[1:].
|
||
|
|
||
|
The return value is an exit code to be passed to sys.exit(); it
|
||
|
may be None to indicate success.
|
||
|
|
||
|
When an exception happens during timing, a traceback is printed to
|
||
|
stderr and the return value is 1. Exceptions at other times
|
||
|
(including the template compilation) are not caught.
|
||
|
|
||
|
'_wrap_timer' is an internal interface used for unit testing. If it
|
||
|
is not None, it must be a callable that accepts a timer function
|
||
|
and returns another timer function (used for unit testing).
|
||
|
"""
|
||
|
if args is None:
|
||
|
args = sys.argv[1:]
|
||
|
import getopt
|
||
|
try:
|
||
|
opts, args = getopt.getopt(args, "n:s:r:tcvh",
|
||
|
["number=", "setup=", "repeat=",
|
||
|
"time", "clock", "verbose", "help"])
|
||
|
except getopt.error, err:
|
||
|
print err
|
||
|
print "use -h/--help for command line help"
|
||
|
return 2
|
||
|
timer = default_timer
|
||
|
stmt = "\n".join(args) or "pass"
|
||
|
number = 0 # auto-determine
|
||
|
setup = []
|
||
|
repeat = default_repeat
|
||
|
verbose = 0
|
||
|
precision = 3
|
||
|
for o, a in opts:
|
||
|
if o in ("-n", "--number"):
|
||
|
number = int(a)
|
||
|
if o in ("-s", "--setup"):
|
||
|
setup.append(a)
|
||
|
if o in ("-r", "--repeat"):
|
||
|
repeat = int(a)
|
||
|
if repeat <= 0:
|
||
|
repeat = 1
|
||
|
if o in ("-t", "--time"):
|
||
|
timer = time.time
|
||
|
if o in ("-c", "--clock"):
|
||
|
timer = time.clock
|
||
|
if o in ("-v", "--verbose"):
|
||
|
if verbose:
|
||
|
precision += 1
|
||
|
verbose += 1
|
||
|
if o in ("-h", "--help"):
|
||
|
print __doc__,
|
||
|
return 0
|
||
|
setup = "\n".join(setup) or "pass"
|
||
|
# Include the current directory, so that local imports work (sys.path
|
||
|
# contains the directory of this script, rather than the current
|
||
|
# directory)
|
||
|
import os
|
||
|
sys.path.insert(0, os.curdir)
|
||
|
if _wrap_timer is not None:
|
||
|
timer = _wrap_timer(timer)
|
||
|
t = Timer(stmt, setup, timer)
|
||
|
if number == 0:
|
||
|
# determine number so that 0.2 <= total time < 2.0
|
||
|
for i in range(1, 10):
|
||
|
number = 10**i
|
||
|
try:
|
||
|
x = t.timeit(number)
|
||
|
except:
|
||
|
t.print_exc()
|
||
|
return 1
|
||
|
if verbose:
|
||
|
print "%d loops -> %.*g secs" % (number, precision, x)
|
||
|
if x >= 0.2:
|
||
|
break
|
||
|
try:
|
||
|
r = t.repeat(repeat, number)
|
||
|
except:
|
||
|
t.print_exc()
|
||
|
return 1
|
||
|
best = min(r)
|
||
|
if verbose:
|
||
|
print "raw times:", " ".join(["%.*g" % (precision, x) for x in r])
|
||
|
print "%d loops," % number,
|
||
|
usec = best * 1e6 / number
|
||
|
if usec < 1000:
|
||
|
print "best of %d: %.*g usec per loop" % (repeat, precision, usec)
|
||
|
else:
|
||
|
msec = usec / 1000
|
||
|
if msec < 1000:
|
||
|
print "best of %d: %.*g msec per loop" % (repeat, precision, msec)
|
||
|
else:
|
||
|
sec = msec / 1000
|
||
|
print "best of %d: %.*g sec per loop" % (repeat, precision, sec)
|
||
|
return None
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
sys.exit(main())
|