blob: 6c3ec01067f2d415a87eda572b8727be383d0141 [file] [log] [blame]
Olivier Deprezf4ef2d02021-04-20 13:36:24 +02001#! /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
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 _t1 = _timer()
76 return _t1 - _t0
77"""
78
79def reindent(src, indent):
80 """Helper to reindent a multi-line statement."""
81 return src.replace("\n", "\n" + " "*indent)
82
83class Timer:
84 """Class for timing execution speed of small code snippets.
85
86 The constructor takes a statement to be timed, an additional
87 statement used for setup, and a timer function. Both statements
88 default to 'pass'; the timer function is platform-dependent (see
89 module doc string). If 'globals' is specified, the code will be
90 executed within that namespace (as opposed to inside timeit's
91 namespace).
92
93 To measure the execution time of the first statement, use the
94 timeit() method. The repeat() method is a convenience to call
95 timeit() multiple times and return a list of results.
96
97 The statements may contain newlines, as long as they don't contain
98 multi-line string literals.
99 """
100
101 def __init__(self, stmt="pass", setup="pass", timer=default_timer,
102 globals=None):
103 """Constructor. See class doc string."""
104 self.timer = timer
105 local_ns = {}
106 global_ns = _globals() if globals is None else globals
107 init = ''
108 if isinstance(setup, str):
109 # Check that the code can be compiled outside a function
110 compile(setup, dummy_src_name, "exec")
111 stmtprefix = setup + '\n'
112 setup = reindent(setup, 4)
113 elif callable(setup):
114 local_ns['_setup'] = setup
115 init += ', _setup=_setup'
116 stmtprefix = ''
117 setup = '_setup()'
118 else:
119 raise ValueError("setup is neither a string nor callable")
120 if isinstance(stmt, str):
121 # Check that the code can be compiled outside a function
122 compile(stmtprefix + stmt, dummy_src_name, "exec")
123 stmt = reindent(stmt, 8)
124 elif callable(stmt):
125 local_ns['_stmt'] = stmt
126 init += ', _stmt=_stmt'
127 stmt = '_stmt()'
128 else:
129 raise ValueError("stmt is neither a string nor callable")
130 src = template.format(stmt=stmt, setup=setup, init=init)
131 self.src = src # Save for traceback display
132 code = compile(src, dummy_src_name, "exec")
133 exec(code, global_ns, local_ns)
134 self.inner = local_ns["inner"]
135
136 def print_exc(self, file=None):
137 """Helper to print a traceback from the timed code.
138
139 Typical use:
140
141 t = Timer(...) # outside the try/except
142 try:
143 t.timeit(...) # or t.repeat(...)
144 except:
145 t.print_exc()
146
147 The advantage over the standard traceback is that source lines
148 in the compiled template will be displayed.
149
150 The optional file argument directs where the traceback is
151 sent; it defaults to sys.stderr.
152 """
153 import linecache, traceback
154 if self.src is not None:
155 linecache.cache[dummy_src_name] = (len(self.src),
156 None,
157 self.src.split("\n"),
158 dummy_src_name)
159 # else the source is already stored somewhere else
160
161 traceback.print_exc(file=file)
162
163 def timeit(self, number=default_number):
164 """Time 'number' executions of the main statement.
165
166 To be precise, this executes the setup statement once, and
167 then returns the time it takes to execute the main statement
168 a number of times, as a float measured in seconds. The
169 argument is the number of times through the loop, defaulting
170 to one million. The main statement, the setup statement and
171 the timer function to be used are passed to the constructor.
172 """
173 it = itertools.repeat(None, number)
174 gcold = gc.isenabled()
175 gc.disable()
176 try:
177 timing = self.inner(it, self.timer)
178 finally:
179 if gcold:
180 gc.enable()
181 return timing
182
183 def repeat(self, repeat=default_repeat, number=default_number):
184 """Call timeit() a few times.
185
186 This is a convenience function that calls the timeit()
187 repeatedly, returning a list of results. The first argument
188 specifies how many times to call timeit(), defaulting to 5;
189 the second argument specifies the timer argument, defaulting
190 to one million.
191
192 Note: it's tempting to calculate mean and standard deviation
193 from the result vector and report these. However, this is not
194 very useful. In a typical case, the lowest value gives a
195 lower bound for how fast your machine can run the given code
196 snippet; higher values in the result vector are typically not
197 caused by variability in Python's speed, but by other
198 processes interfering with your timing accuracy. So the min()
199 of the result is probably the only number you should be
200 interested in. After that, you should look at the entire
201 vector and apply common sense rather than statistics.
202 """
203 r = []
204 for i in range(repeat):
205 t = self.timeit(number)
206 r.append(t)
207 return r
208
209 def autorange(self, callback=None):
210 """Return the number of loops and time taken so that total time >= 0.2.
211
212 Calls the timeit method with increasing numbers from the sequence
213 1, 2, 5, 10, 20, 50, ... until the time taken is at least 0.2
214 second. Returns (number, time_taken).
215
216 If *callback* is given and is not None, it will be called after
217 each trial with two arguments: ``callback(number, time_taken)``.
218 """
219 i = 1
220 while True:
221 for j in 1, 2, 5:
222 number = i * j
223 time_taken = self.timeit(number)
224 if callback:
225 callback(number, time_taken)
226 if time_taken >= 0.2:
227 return (number, time_taken)
228 i *= 10
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
270 timer = default_timer
271 stmt = "\n".join(args) or "pass"
272 number = 0 # auto-determine
273 setup = []
274 repeat = default_repeat
275 verbose = 0
276 time_unit = None
277 units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0}
278 precision = 3
279 for o, a in opts:
280 if o in ("-n", "--number"):
281 number = int(a)
282 if o in ("-s", "--setup"):
283 setup.append(a)
284 if o in ("-u", "--unit"):
285 if a in units:
286 time_unit = a
287 else:
288 print("Unrecognized unit. Please select nsec, usec, msec, or sec.",
289 file=sys.stderr)
290 return 2
291 if o in ("-r", "--repeat"):
292 repeat = int(a)
293 if repeat <= 0:
294 repeat = 1
295 if o in ("-p", "--process"):
296 timer = time.process_time
297 if o in ("-v", "--verbose"):
298 if verbose:
299 precision += 1
300 verbose += 1
301 if o in ("-h", "--help"):
302 print(__doc__, end=' ')
303 return 0
304 setup = "\n".join(setup) or "pass"
305
306 # Include the current directory, so that local imports work (sys.path
307 # contains the directory of this script, rather than the current
308 # directory)
309 import os
310 sys.path.insert(0, os.curdir)
311 if _wrap_timer is not None:
312 timer = _wrap_timer(timer)
313
314 t = Timer(stmt, setup, timer)
315 if number == 0:
316 # determine number so that 0.2 <= total time < 2.0
317 callback = None
318 if verbose:
319 def callback(number, time_taken):
320 msg = "{num} loop{s} -> {secs:.{prec}g} secs"
321 plural = (number != 1)
322 print(msg.format(num=number, s='s' if plural else '',
323 secs=time_taken, prec=precision))
324 try:
325 number, _ = t.autorange(callback)
326 except:
327 t.print_exc()
328 return 1
329
330 if verbose:
331 print()
332
333 try:
334 raw_timings = t.repeat(repeat, number)
335 except:
336 t.print_exc()
337 return 1
338
339 def format_time(dt):
340 unit = time_unit
341
342 if unit is not None:
343 scale = units[unit]
344 else:
345 scales = [(scale, unit) for unit, scale in units.items()]
346 scales.sort(reverse=True)
347 for scale, unit in scales:
348 if dt >= scale:
349 break
350
351 return "%.*g %s" % (precision, dt / scale, unit)
352
353 if verbose:
354 print("raw times: %s" % ", ".join(map(format_time, raw_timings)))
355 print()
356 timings = [dt / number for dt in raw_timings]
357
358 best = min(timings)
359 print("%d loop%s, best of %d: %s per loop"
360 % (number, 's' if number != 1 else '',
361 repeat, format_time(best)))
362
363 best = min(timings)
364 worst = max(timings)
365 if worst >= best * 4:
366 import warnings
367 warnings.warn_explicit("The test results are likely unreliable. "
368 "The worst time (%s) was more than four times "
369 "slower than the best time (%s)."
370 % (format_time(worst), format_time(best)),
371 UserWarning, '', 0)
372 return None
373
374if __name__ == "__main__":
375 sys.exit(main())