• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#! /usr/bin/env python3
2
3"""Tool for measuring execution time of small code snippets.
4
5This module avoids a number of common traps for measuring execution
6times.  See also Tim Peters' introduction to the Algorithms chapter in
7the Python Cookbook, published by O'Reilly.
8
9Library usage: see the Timer class.
10
11Command line usage:
12    python timeit.py [-n N] [-r N] [-s S] [-t] [-c] [-p] [-h] [--] [statement]
13
14Options:
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
28A multi-line statement may be given by specifying each line as a
29separate argument; indented lines are possible by enclosing an
30argument in quotes and using leading spaces.  Multiple -s options are
31treated similarly.
32
33If -n is not given, a suitable number of loops is calculated by trying
34successive powers of 10 until the total time is at least 0.2 seconds.
35
36Note: there is a certain baseline overhead associated with executing a
37pass statement.  It differs between versions.  The code here doesn't try
38to hide it, but you should be aware of it.  The baseline overhead can be
39measured by invoking the program without arguments.
40
41Classes:
42
43    Timer
44
45Functions:
46
47    timeit(string, string) -> float
48    repeat(string, string) -> list
49    default_timer() -> float
50
51"""
52
53import gc
54import sys
55import time
56import itertools
57
58__all__ = ["Timer", "timeit", "repeat", "default_timer"]
59
60dummy_src_name = "<timeit-src>"
61default_number = 1000000
62default_repeat = 3
63default_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.
70template = """
71def 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
80def reindent(src, indent):
81    """Helper to reindent a multi-line statement."""
82    return src.replace("\n", "\n" + " "*indent)
83
84class 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
230def 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
235def 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
240def 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
361if __name__ == "__main__":
362    sys.exit(main())
363