| #! /usr/bin/env python3 |
| |
| """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() |
| -c/--clock: use time.clock() |
| -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 |
| default_timer = time.perf_counter |
| |
| # 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): |
| {setup} |
| _t0 = _timer() |
| for _i in _it: |
| {stmt} |
| _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, str): |
| stmt = reindent(stmt, 8) |
| if isinstance(setup, str): |
| setup = reindent(setup, 4) |
| src = template.format(stmt=stmt, setup=setup) |
| elif callable(setup): |
| src = template.format(stmt=stmt, 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, globals(), ns) |
| self.inner = ns["inner"] |
| elif callable(stmt): |
| self.src = None |
| if isinstance(setup, str): |
| _setup = setup |
| def setup(): |
| exec(_setup, globals(), ns) |
| elif not callable(setup): |
| 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 as 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__, end=' ') |
| 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, end=' ') |
| 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()) |