timeit.py 10 KB
Newer Older
1 2
#! /usr/bin/env python

3
"""Tool for measuring execution time of small code snippets.
4

5 6 7
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.
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] [-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 19
  -t/--time: use time.time() (default on Unix)
  -c/--clock: use time.clock() (default on Windows)
20
  -v/--verbose: print raw timing results; repeat for more digits precision
21
  -h/--help: print this usage message and exit
22
  statement: statement to be timed (default 'pass')
23 24 25

A multi-line statement may be given by specifying each line as a
separate argument; indented lines are possible by enclosing an
26 27
argument in quotes and using leading spaces.  Multiple -s options are
treated similarly.
28 29 30 31 32 33 34 35

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
36
functions measure wall clock time, not the CPU time.  This means that
37 38
other processes running on the same computer may interfere with the
timing.  The best thing to do when accurate timing is necessary is to
39 40 41
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.
42 43 44

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
45 46
be aware of it.  The baseline overhead can be measured by invoking the
program without arguments.
47

48 49 50 51 52
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.
"""
53

54
import gc
55 56
import sys
import time
57 58 59 60 61
try:
    import itertools
except ImportError:
    # Must be an older Python version (see timeit() below)
    itertools = None
62 63 64

__all__ = ["Timer"]

65
dummy_src_name = "<timeit-src>"
66
default_number = 1000000
67
default_repeat = 3
68 69 70 71 72 73 74 75

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

76 77 78
# 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.
79
template = """
80
def inner(_it, _timer):
81
    %(setup)s
82
    _t0 = _timer()
83
    for _i in _it:
84
        %(stmt)s
85 86
    _t1 = _timer()
    return _t1 - _t0
87 88 89
"""

def reindent(src, indent):
90
    """Helper to reindent a multi-line statement."""
91
    return src.replace("\n", "\n" + " "*indent)
92 93

class Timer:
94 95 96 97 98 99 100 101 102 103 104 105 106 107
    """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.
    """
108 109

    def __init__(self, stmt="pass", setup="pass", timer=default_timer):
110
        """Constructor.  See class doc string."""
111 112 113 114
        self.timer = timer
        stmt = reindent(stmt, 8)
        setup = reindent(setup, 4)
        src = template % {'stmt': stmt, 'setup': setup}
115 116
        self.src = src # Save for traceback display
        code = compile(src, dummy_src_name, "exec")
117 118 119 120
        ns = {}
        exec code in globals(), ns
        self.inner = ns["inner"]

121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
    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
        linecache.cache[dummy_src_name] = (len(self.src),
                                           None,
                                           self.src.split("\n"),
                                           dummy_src_name)
        traceback.print_exc(file=file)

145
    def timeit(self, number=default_number):
146 147 148 149 150 151 152 153 154
        """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.
        """
155
        if itertools:
156
            it = itertools.repeat(None, number)
157
        else:
158
            it = [None] * number
159 160 161 162 163 164
        gcold = gc.isenabled()
        gc.disable()
        timing = self.inner(it, self.timer)
        if gcold:
            gc.enable()
        return timing
165 166

    def repeat(self, repeat=default_repeat, number=default_number):
167
        """Call timeit() a few times.
168

169
        This is a convenience function that calls the timeit()
170
        repeatedly, returning a list of results.  The first argument
171
        specifies how many times to call timeit(), defaulting to 3;
172 173
        the second argument specifies the timer argument, defaulting
        to one million.
174 175 176 177 178 179 180 181 182 183 184

        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.
185
        """
186 187 188 189 190 191 192
        r = []
        for i in range(repeat):
            t = self.timeit(number)
            r.append(t)
        return r

def main(args=None):
193 194 195 196 197 198 199
    """Main program, used when run as a script.

    The optional 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.
200 201 202 203

    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.
204
    """
205 206 207 208
    if args is None:
        args = sys.argv[1:]
    import getopt
    try:
209
        opts, args = getopt.getopt(args, "n:s:r:tcvh",
210
                                   ["number=", "setup=", "repeat=",
211
                                    "time", "clock", "verbose", "help"])
212 213
    except getopt.error, err:
        print err
214
        print "use -h/--help for command line help"
215 216 217 218
        return 2
    timer = default_timer
    stmt = "\n".join(args) or "pass"
    number = 0 # auto-determine
219
    setup = []
220 221 222
    repeat = default_repeat
    verbose = 0
    precision = 3
223 224 225 226
    for o, a in opts:
        if o in ("-n", "--number"):
            number = int(a)
        if o in ("-s", "--setup"):
227
            setup.append(a)
228 229 230 231
        if o in ("-r", "--repeat"):
            repeat = int(a)
            if repeat <= 0:
                repeat = 1
232
        if o in ("-t", "--time"):
233
            timer = time.time
234
        if o in ("-c", "--clock"):
235
            timer = time.clock
236 237 238 239
        if o in ("-v", "--verbose"):
            if verbose:
                precision += 1
            verbose += 1
240 241 242
        if o in ("-h", "--help"):
            print __doc__,
            return 0
243
    setup = "\n".join(setup) or "pass"
244 245 246 247 248
    # 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)
249 250 251 252 253
    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
254 255 256 257 258
            try:
                x = t.timeit(number)
            except:
                t.print_exc()
                return 1
259 260
            if verbose:
                print "%d loops -> %.*g secs" % (number, precision, x)
261 262
            if x >= 0.2:
                break
263 264 265 266 267
    try:
        r = t.repeat(repeat, number)
    except:
        t.print_exc()
        return 1
268
    best = min(r)
269 270
    if verbose:
        print "raw times:", " ".join(["%.*g" % (precision, x) for x in r])
271 272
    print "%d loops," % number,
    usec = best * 1e6 / number
273 274 275 276 277 278 279 280 281
    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)
282
    return None
283 284 285

if __name__ == "__main__":
    sys.exit(main())