1#!/usr/local/bin/python3.10
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
32increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the
33total time is at least 0.2 seconds.
34
35Note: there is a certain baseline overhead associated with executing a
36pass statement.  It differs between versions.  The code here doesn't try
37to hide it, but you should be aware of it.  The baseline overhead can be
38measured by invoking the program without arguments.
39
40Classes:
41
42    Timer
43
44Functions:
45
46    timeit(string, string) -> float
47    repeat(string, string) -> list
48    default_timer() -> float
49
50"""
51
52import gc
53import sys
54import time
55import itertools
56
57__all__ = ["Timer", "timeit", "repeat", "default_timer"]
58
59dummy_src_name = "<timeit-src>"
60default_number = 1000000
61default_repeat = 5
62default_timer = time.perf_counter
63
64_globals = globals
65
66# Don't change the indentation of the template; the reindent() calls
67# in Timer.__init__() depend on setup being indented 4 spaces and stmt
68# being indented 8 spaces.
69template = """
70def inner(_it, _timer{init}):
71    {setup}
72    _t0 = _timer()
73    for _i in _it:
74        {stmt}
75        pass
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 5;
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 increasing numbers from the sequence
214        1, 2, 5, 10, 20, 50, ... until the time taken is at least 0.2
215        second.  Returns (number, time_taken).
216
217        If *callback* is given and is not None, it will be called after
218        each trial with two arguments: ``callback(number, time_taken)``.
219        """
220        i = 1
221        while True:
222            for j in 1, 2, 5:
223                number = i * j
224                time_taken = self.timeit(number)
225                if callback:
226                    callback(number, time_taken)
227                if time_taken >= 0.2:
228                    return (number, time_taken)
229            i *= 10
230
231def timeit(stmt="pass", setup="pass", timer=default_timer,
232           number=default_number, globals=None):
233    """Convenience function to create Timer object and call timeit method."""
234    return Timer(stmt, setup, timer, globals).timeit(number)
235
236def repeat(stmt="pass", setup="pass", timer=default_timer,
237           repeat=default_repeat, number=default_number, globals=None):
238    """Convenience function to create Timer object and call repeat method."""
239    return Timer(stmt, setup, timer, globals).repeat(repeat, number)
240
241def main(args=None, *, _wrap_timer=None):
242    """Main program, used when run as a script.
243
244    The optional 'args' argument specifies the command line to be parsed,
245    defaulting to sys.argv[1:].
246
247    The return value is an exit code to be passed to sys.exit(); it
248    may be None to indicate success.
249
250    When an exception happens during timing, a traceback is printed to
251    stderr and the return value is 1.  Exceptions at other times
252    (including the template compilation) are not caught.
253
254    '_wrap_timer' is an internal interface used for unit testing.  If it
255    is not None, it must be a callable that accepts a timer function
256    and returns another timer function (used for unit testing).
257    """
258    if args is None:
259        args = sys.argv[1:]
260    import getopt
261    try:
262        opts, args = getopt.getopt(args, "n:u:s:r:tcpvh",
263                                   ["number=", "setup=", "repeat=",
264                                    "time", "clock", "process",
265                                    "verbose", "unit=", "help"])
266    except getopt.error as err:
267        print(err)
268        print("use -h/--help for command line help")
269        return 2
270
271    timer = default_timer
272    stmt = "\n".join(args) or "pass"
273    number = 0 # auto-determine
274    setup = []
275    repeat = default_repeat
276    verbose = 0
277    time_unit = None
278    units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0}
279    precision = 3
280    for o, a in opts:
281        if o in ("-n", "--number"):
282            number = int(a)
283        if o in ("-s", "--setup"):
284            setup.append(a)
285        if o in ("-u", "--unit"):
286            if a in units:
287                time_unit = a
288            else:
289                print("Unrecognized unit. Please select nsec, usec, msec, or sec.",
290                    file=sys.stderr)
291                return 2
292        if o in ("-r", "--repeat"):
293            repeat = int(a)
294            if repeat <= 0:
295                repeat = 1
296        if o in ("-p", "--process"):
297            timer = time.process_time
298        if o in ("-v", "--verbose"):
299            if verbose:
300                precision += 1
301            verbose += 1
302        if o in ("-h", "--help"):
303            print(__doc__, end=' ')
304            return 0
305    setup = "\n".join(setup) or "pass"
306
307    # Include the current directory, so that local imports work (sys.path
308    # contains the directory of this script, rather than the current
309    # directory)
310    import os
311    sys.path.insert(0, os.curdir)
312    if _wrap_timer is not None:
313        timer = _wrap_timer(timer)
314
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} loop{s} -> {secs:.{prec}g} secs"
322                plural = (number != 1)
323                print(msg.format(num=number, s='s' if plural else '',
324                                  secs=time_taken, prec=precision))
325        try:
326            number, _ = t.autorange(callback)
327        except:
328            t.print_exc()
329            return 1
330
331        if verbose:
332            print()
333
334    try:
335        raw_timings = t.repeat(repeat, number)
336    except:
337        t.print_exc()
338        return 1
339
340    def format_time(dt):
341        unit = time_unit
342
343        if unit is not None:
344            scale = units[unit]
345        else:
346            scales = [(scale, unit) for unit, scale in units.items()]
347            scales.sort(reverse=True)
348            for scale, unit in scales:
349                if dt >= scale:
350                    break
351
352        return "%.*g %s" % (precision, dt / scale, unit)
353
354    if verbose:
355        print("raw times: %s" % ", ".join(map(format_time, raw_timings)))
356        print()
357    timings = [dt / number for dt in raw_timings]
358
359    best = min(timings)
360    print("%d loop%s, best of %d: %s per loop"
361          % (number, 's' if number != 1 else '',
362             repeat, format_time(best)))
363
364    best = min(timings)
365    worst = max(timings)
366    if worst >= best * 4:
367        import warnings
368        warnings.warn_explicit("The test results are likely unreliable. "
369                               "The worst time (%s) was more than four times "
370                               "slower than the best time (%s)."
371                               % (format_time(worst), format_time(best)),
372                               UserWarning, '', 0)
373    return None
374
375if __name__ == "__main__":
376    sys.exit(main())
377