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