| #! /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] [-p] [-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'). |
| Execution time of this setup statement is NOT timed. |
| -p/--process: use time.process_time() (default is time.perf_counter()) |
| -v/--verbose: print raw timing results; repeat for more digits precision |
| -u/--unit: set the output time unit (nsec, usec, msec, or sec) |
| -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. |
| |
| Note: there is a certain baseline overhead associated with executing a |
| pass statement. It differs between versions. 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. |
| |
| Classes: |
| |
| Timer |
| |
| Functions: |
| |
| timeit(string, string) -> float |
| repeat(string, string) -> list |
| default_timer() -> float |
| |
| """ |
| |
| import gc |
| import sys |
| import time |
| import itertools |
| |
| __all__ = ["Timer", "timeit", "repeat", "default_timer"] |
| |
| dummy_src_name = "<timeit-src>" |
| default_number = 1000000 |
| default_repeat = 5 |
| default_timer = time.perf_counter |
| |
| _globals = globals |
| |
| # 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}): |
| {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) |
| |
| 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). If 'globals' is specified, the code will be |
| executed within that namespace (as opposed to inside timeit's |
| namespace). |
| |
| 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, |
| globals=None): |
| """Constructor. See class doc string.""" |
| self.timer = timer |
| local_ns = {} |
| global_ns = _globals() if globals is None else globals |
| init = '' |
| if isinstance(setup, str): |
| # Check that the code can be compiled outside a function |
| compile(setup, dummy_src_name, "exec") |
| stmtprefix = setup + '\n' |
| setup = reindent(setup, 4) |
| elif callable(setup): |
| local_ns['_setup'] = setup |
| init += ', _setup=_setup' |
| stmtprefix = '' |
| setup = '_setup()' |
| else: |
| raise ValueError("setup is neither a string nor callable") |
| if isinstance(stmt, str): |
| # Check that the code can be compiled outside a function |
| compile(stmtprefix + stmt, dummy_src_name, "exec") |
| stmt = reindent(stmt, 8) |
| elif callable(stmt): |
| local_ns['_stmt'] = stmt |
| init += ', _stmt=_stmt' |
| stmt = '_stmt()' |
| else: |
| raise ValueError("stmt is neither a string nor callable") |
| src = template.format(stmt=stmt, setup=setup, init=init) |
| self.src = src # Save for traceback display |
| code = compile(src, dummy_src_name, "exec") |
| exec(code, global_ns, local_ns) |
| self.inner = local_ns["inner"] |
| |
| 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. |
| """ |
| it = itertools.repeat(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 autorange(self, callback=None): |
| """Return the number of loops so that total time >= 0.2. |
| |
| Calls the timeit method with increasing numbers from the sequence |
| 1, 2, 5, 10, 20, 50, ... until the time taken is at least 0.2 |
| second. Returns (number, time_taken). |
| |
| If *callback* is given and is not None, it will be called after |
| each trial with two arguments: ``callback(number, time_taken)``. |
| """ |
| i = 1 |
| while True: |
| for j in 1, 2, 5: |
| number = i * j |
| time_taken = self.timeit(number) |
| if callback: |
| callback(number, time_taken) |
| if time_taken >= 0.2: |
| return (number, time_taken) |
| i *= 10 |
| |
| def timeit(stmt="pass", setup="pass", timer=default_timer, |
| number=default_number, globals=None): |
| """Convenience function to create Timer object and call timeit method.""" |
| return Timer(stmt, setup, timer, globals).timeit(number) |
| |
| def repeat(stmt="pass", setup="pass", timer=default_timer, |
| repeat=default_repeat, number=default_number, globals=None): |
| """Convenience function to create Timer object and call repeat method.""" |
| return Timer(stmt, setup, timer, globals).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:u:s:r:tcpvh", |
| ["number=", "setup=", "repeat=", |
| "time", "clock", "process", |
| "verbose", "unit=", "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 |
| time_unit = None |
| units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.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 ("-u", "--unit"): |
| if a in units: |
| time_unit = a |
| else: |
| print("Unrecognized unit. Please select nsec, usec, msec, or sec.", |
| file=sys.stderr) |
| return 2 |
| if o in ("-r", "--repeat"): |
| repeat = int(a) |
| if repeat <= 0: |
| repeat = 1 |
| if o in ("-p", "--process"): |
| timer = time.process_time |
| 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 |
| callback = None |
| if verbose: |
| def callback(number, time_taken): |
| msg = "{num} loop{s} -> {secs:.{prec}g} secs" |
| plural = (number != 1) |
| print(msg.format(num=number, s='s' if plural else '', |
| secs=time_taken, prec=precision)) |
| try: |
| number, _ = t.autorange(callback) |
| except: |
| t.print_exc() |
| return 1 |
| |
| if verbose: |
| print() |
| |
| try: |
| raw_timings = t.repeat(repeat, number) |
| except: |
| t.print_exc() |
| return 1 |
| |
| def format_time(dt): |
| unit = time_unit |
| |
| if unit is not None: |
| scale = units[unit] |
| else: |
| scales = [(scale, unit) for unit, scale in units.items()] |
| scales.sort(reverse=True) |
| for scale, unit in scales: |
| if dt >= scale: |
| break |
| |
| return "%.*g %s" % (precision, dt / scale, unit) |
| |
| if verbose: |
| print("raw times: %s" % ", ".join(map(format_time, raw_timings))) |
| print() |
| timings = [dt / number for dt in raw_timings] |
| |
| best = min(timings) |
| print("%d loop%s, best of %d: %s per loop" |
| % (number, 's' if number != 1 else '', |
| repeat, format_time(best))) |
| |
| best = min(timings) |
| worst = max(timings) |
| if worst >= best * 4: |
| import warnings |
| warnings.warn_explicit("The test results are likely unreliable. " |
| "The worst time (%s) was more than four times " |
| "slower than the best time (%s)." |
| % (format_time(worst), format_time(best)), |
| UserWarning, '', 0) |
| return None |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |