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