Home | History | Annotate | Download | only in Lib
      1 #! /usr/bin/env python3
      2 
      3 """Tool for measuring execution time of small code snippets.
      4 
      5 This module avoids a number of common traps for measuring execution
      6 times.  See also Tim Peters' introduction to the Algorithms chapter in
      7 the Python Cookbook, published by O'Reilly.
      8 
      9 Library usage: see the Timer class.
     10 
     11 Command line usage:
     12     python timeit.py [-n N] [-r N] [-s S] [-t] [-c] [-p] [-h] [--] [statement]
     13 
     14 Options:
     15   -n/--number N: how many times to execute 'statement' (default: see below)
     16   -r/--repeat N: how many times to repeat the timer (default 3)
     17   -s/--setup S: statement to be executed once initially (default 'pass').
     18                 Execution time of this setup statement is NOT timed.
     19   -p/--process: use time.process_time() (default is time.perf_counter())
     20   -t/--time: use time.time() (deprecated)
     21   -c/--clock: use time.clock() (deprecated)
     22   -v/--verbose: print raw timing results; repeat for more digits precision
     23   -u/--unit: set the output time unit (usec, msec, or sec)
     24   -h/--help: print this usage message and exit
     25   --: separate options from statement, use when statement starts with -
     26   statement: statement to be timed (default 'pass')
     27 
     28 A multi-line statement may be given by specifying each line as a
     29 separate argument; indented lines are possible by enclosing an
     30 argument in quotes and using leading spaces.  Multiple -s options are
     31 treated similarly.
     32 
     33 If -n is not given, a suitable number of loops is calculated by trying
     34 successive powers of 10 until the total time is at least 0.2 seconds.
     35 
     36 Note: there is a certain baseline overhead associated with executing a
     37 pass statement.  It differs between versions.  The code here doesn't try
     38 to hide it, but you should be aware of it.  The baseline overhead can be
     39 measured by invoking the program without arguments.
     40 
     41 Classes:
     42 
     43     Timer
     44 
     45 Functions:
     46 
     47     timeit(string, string) -> float
     48     repeat(string, string) -> list
     49     default_timer() -> float
     50 
     51 """
     52 
     53 import gc
     54 import sys
     55 import time
     56 import itertools
     57 
     58 __all__ = ["Timer", "timeit", "repeat", "default_timer"]
     59 
     60 dummy_src_name = "<timeit-src>"
     61 default_number = 1000000
     62 default_repeat = 3
     63 default_timer = time.perf_counter
     64 
     65 _globals = globals
     66 
     67 # Don't change the indentation of the template; the reindent() calls
     68 # in Timer.__init__() depend on setup being indented 4 spaces and stmt
     69 # being indented 8 spaces.
     70 template = """
     71 def inner(_it, _timer{init}):
     72     {setup}
     73     _t0 = _timer()
     74     for _i in _it:
     75         {stmt}
     76     _t1 = _timer()
     77     return _t1 - _t0
     78 """
     79 
     80 def reindent(src, indent):
     81     """Helper to reindent a multi-line statement."""
     82     return src.replace("\n", "\n" + " "*indent)
     83 
     84 class Timer:
     85     """Class for timing execution speed of small code snippets.
     86 
     87     The constructor takes a statement to be timed, an additional
     88     statement used for setup, and a timer function.  Both statements
     89     default to 'pass'; the timer function is platform-dependent (see
     90     module doc string).  If 'globals' is specified, the code will be
     91     executed within that namespace (as opposed to inside timeit's
     92     namespace).
     93 
     94     To measure the execution time of the first statement, use the
     95     timeit() method.  The repeat() method is a convenience to call
     96     timeit() multiple times and return a list of results.
     97 
     98     The statements may contain newlines, as long as they don't contain
     99     multi-line string literals.
    100     """
    101 
    102     def __init__(self, stmt="pass", setup="pass", timer=default_timer,
    103                  globals=None):
    104         """Constructor.  See class doc string."""
    105         self.timer = timer
    106         local_ns = {}
    107         global_ns = _globals() if globals is None else globals
    108         init = ''
    109         if isinstance(setup, str):
    110             # Check that the code can be compiled outside a function
    111             compile(setup, dummy_src_name, "exec")
    112             stmtprefix = setup + '\n'
    113             setup = reindent(setup, 4)
    114         elif callable(setup):
    115             local_ns['_setup'] = setup
    116             init += ', _setup=_setup'
    117             stmtprefix = ''
    118             setup = '_setup()'
    119         else:
    120             raise ValueError("setup is neither a string nor callable")
    121         if isinstance(stmt, str):
    122             # Check that the code can be compiled outside a function
    123             compile(stmtprefix + stmt, dummy_src_name, "exec")
    124             stmt = reindent(stmt, 8)
    125         elif callable(stmt):
    126             local_ns['_stmt'] = stmt
    127             init += ', _stmt=_stmt'
    128             stmt = '_stmt()'
    129         else:
    130             raise ValueError("stmt is neither a string nor callable")
    131         src = template.format(stmt=stmt, setup=setup, init=init)
    132         self.src = src  # Save for traceback display
    133         code = compile(src, dummy_src_name, "exec")
    134         exec(code, global_ns, local_ns)
    135         self.inner = local_ns["inner"]
    136 
    137     def print_exc(self, file=None):
    138         """Helper to print a traceback from the timed code.
    139 
    140         Typical use:
    141 
    142             t = Timer(...)       # outside the try/except
    143             try:
    144                 t.timeit(...)    # or t.repeat(...)
    145             except:
    146                 t.print_exc()
    147 
    148         The advantage over the standard traceback is that source lines
    149         in the compiled template will be displayed.
    150 
    151         The optional file argument directs where the traceback is
    152         sent; it defaults to sys.stderr.
    153         """
    154         import linecache, traceback
    155         if self.src is not None:
    156             linecache.cache[dummy_src_name] = (len(self.src),
    157                                                None,
    158                                                self.src.split("\n"),
    159                                                dummy_src_name)
    160         # else the source is already stored somewhere else
    161 
    162         traceback.print_exc(file=file)
    163 
    164     def timeit(self, number=default_number):
    165         """Time 'number' executions of the main statement.
    166 
    167         To be precise, this executes the setup statement once, and
    168         then returns the time it takes to execute the main statement
    169         a number of times, as a float measured in seconds.  The
    170         argument is the number of times through the loop, defaulting
    171         to one million.  The main statement, the setup statement and
    172         the timer function to be used are passed to the constructor.
    173         """
    174         it = itertools.repeat(None, number)
    175         gcold = gc.isenabled()
    176         gc.disable()
    177         try:
    178             timing = self.inner(it, self.timer)
    179         finally:
    180             if gcold:
    181                 gc.enable()
    182         return timing
    183 
    184     def repeat(self, repeat=default_repeat, number=default_number):
    185         """Call timeit() a few times.
    186 
    187         This is a convenience function that calls the timeit()
    188         repeatedly, returning a list of results.  The first argument
    189         specifies how many times to call timeit(), defaulting to 3;
    190         the second argument specifies the timer argument, defaulting
    191         to one million.
    192 
    193         Note: it's tempting to calculate mean and standard deviation
    194         from the result vector and report these.  However, this is not
    195         very useful.  In a typical case, the lowest value gives a
    196         lower bound for how fast your machine can run the given code
    197         snippet; higher values in the result vector are typically not
    198         caused by variability in Python's speed, but by other
    199         processes interfering with your timing accuracy.  So the min()
    200         of the result is probably the only number you should be
    201         interested in.  After that, you should look at the entire
    202         vector and apply common sense rather than statistics.
    203         """
    204         r = []
    205         for i in range(repeat):
    206             t = self.timeit(number)
    207             r.append(t)
    208         return r
    209 
    210     def autorange(self, callback=None):
    211         """Return the number of loops and time taken so that total time >= 0.2.
    212 
    213         Calls the timeit method with *number* set to successive powers of
    214         ten (10, 100, 1000, ...) up to a maximum of one billion, until
    215         the time taken is at least 0.2 second, or the maximum is reached.
    216         Returns ``(number, time_taken)``.
    217 
    218         If *callback* is given and is not None, it will be called after
    219         each trial with two arguments: ``callback(number, time_taken)``.
    220         """
    221         for i in range(1, 10):
    222             number = 10**i
    223             time_taken = self.timeit(number)
    224             if callback:
    225                 callback(number, time_taken)
    226             if time_taken >= 0.2:
    227                 break
    228         return (number, time_taken)
    229 
    230 def timeit(stmt="pass", setup="pass", timer=default_timer,
    231            number=default_number, globals=None):
    232     """Convenience function to create Timer object and call timeit method."""
    233     return Timer(stmt, setup, timer, globals).timeit(number)
    234 
    235 def repeat(stmt="pass", setup="pass", timer=default_timer,
    236            repeat=default_repeat, number=default_number, globals=None):
    237     """Convenience function to create Timer object and call repeat method."""
    238     return Timer(stmt, setup, timer, globals).repeat(repeat, number)
    239 
    240 def main(args=None, *, _wrap_timer=None):
    241     """Main program, used when run as a script.
    242 
    243     The optional 'args' argument specifies the command line to be parsed,
    244     defaulting to sys.argv[1:].
    245 
    246     The return value is an exit code to be passed to sys.exit(); it
    247     may be None to indicate success.
    248 
    249     When an exception happens during timing, a traceback is printed to
    250     stderr and the return value is 1.  Exceptions at other times
    251     (including the template compilation) are not caught.
    252 
    253     '_wrap_timer' is an internal interface used for unit testing.  If it
    254     is not None, it must be a callable that accepts a timer function
    255     and returns another timer function (used for unit testing).
    256     """
    257     if args is None:
    258         args = sys.argv[1:]
    259     import getopt
    260     try:
    261         opts, args = getopt.getopt(args, "n:u:s:r:tcpvh",
    262                                    ["number=", "setup=", "repeat=",
    263                                     "time", "clock", "process",
    264                                     "verbose", "unit=", "help"])
    265     except getopt.error as err:
    266         print(err)
    267         print("use -h/--help for command line help")
    268         return 2
    269     timer = default_timer
    270     stmt = "\n".join(args) or "pass"
    271     number = 0 # auto-determine
    272     setup = []
    273     repeat = default_repeat
    274     verbose = 0
    275     time_unit = None
    276     units = {"usec": 1, "msec": 1e3, "sec": 1e6}
    277     precision = 3
    278     for o, a in opts:
    279         if o in ("-n", "--number"):
    280             number = int(a)
    281         if o in ("-s", "--setup"):
    282             setup.append(a)
    283         if o in ("-u", "--unit"):
    284             if a in units:
    285                 time_unit = a
    286             else:
    287                 print("Unrecognized unit. Please select usec, msec, or sec.",
    288                     file=sys.stderr)
    289                 return 2
    290         if o in ("-r", "--repeat"):
    291             repeat = int(a)
    292             if repeat <= 0:
    293                 repeat = 1
    294         if o in ("-t", "--time"):
    295             timer = time.time
    296         if o in ("-c", "--clock"):
    297             timer = time.clock
    298         if o in ("-p", "--process"):
    299             timer = time.process_time
    300         if o in ("-v", "--verbose"):
    301             if verbose:
    302                 precision += 1
    303             verbose += 1
    304         if o in ("-h", "--help"):
    305             print(__doc__, end=' ')
    306             return 0
    307     setup = "\n".join(setup) or "pass"
    308     # Include the current directory, so that local imports work (sys.path
    309     # contains the directory of this script, rather than the current
    310     # directory)
    311     import os
    312     sys.path.insert(0, os.curdir)
    313     if _wrap_timer is not None:
    314         timer = _wrap_timer(timer)
    315     t = Timer(stmt, setup, timer)
    316     if number == 0:
    317         # determine number so that 0.2 <= total time < 2.0
    318         callback = None
    319         if verbose:
    320             def callback(number, time_taken):
    321                 msg = "{num} loops -> {secs:.{prec}g} secs"
    322                 print(msg.format(num=number, secs=time_taken, prec=precision))
    323         try:
    324             number, _ = t.autorange(callback)
    325         except:
    326             t.print_exc()
    327             return 1
    328     try:
    329         r = t.repeat(repeat, number)
    330     except:
    331         t.print_exc()
    332         return 1
    333     best = min(r)
    334     if verbose:
    335         print("raw times:", " ".join(["%.*g" % (precision, x) for x in r]))
    336     print("%d loops," % number, end=' ')
    337     usec = best * 1e6 / number
    338     if time_unit is not None:
    339         scale = units[time_unit]
    340     else:
    341         scales = [(scale, unit) for unit, scale in units.items()]
    342         scales.sort(reverse=True)
    343         for scale, time_unit in scales:
    344             if usec >= scale:
    345                 break
    346     print("best of %d: %.*g %s per loop" % (repeat, precision,
    347                                             usec/scale, time_unit))
    348     best = min(r)
    349     usec = best * 1e6 / number
    350     worst = max(r)
    351     if worst >= best * 4:
    352         usec = worst * 1e6 / number
    353         import warnings
    354         warnings.warn_explicit(
    355             "The test results are likely unreliable. The worst\n"
    356             "time (%.*g %s) was more than four times slower than the best time." %
    357             (precision, usec/scale, time_unit),
    358              UserWarning, '', 0)
    359     return None
    360 
    361 if __name__ == "__main__":
    362     sys.exit(main())
    363