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:tcpvh",
268 ["number=", "setup=", "repeat=",
269 "time", "clock", "process",
270 "verbose", "unit=", "help"])
271 except getopt.error as err:
272 print(err)
273 print("use -h/--help for command line help")
274 return 2
275
276 timer = default_timer
277 stmt = "\n".join(args) or "pass"
278 number = 0 # auto-determine
279 setup = []
280 repeat = default_repeat
281 verbose = 0
282 time_unit = None
283 units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0}
284 precision = 3
285 for o, a in opts:
286 if o in ("-n", "--number"):
287 number = int(a)
288 if o in ("-s", "--setup"):
289 setup.append(a)
290 if o in ("-u", "--unit"):
291 if a in units:
292 time_unit = a
293 else:
294 print("Unrecognized unit. Please select nsec, usec, msec, or sec.",
295 file=sys.stderr)
296 return 2
297 if o in ("-r", "--repeat"):
298 repeat = int(a)
299 if repeat <= 0:
300 repeat = 1
301 if o in ("-p", "--process"):
302 timer = time.process_time
303 if o in ("-v", "--verbose"):
304 if verbose:
305 precision += 1
306 verbose += 1
307 if o in ("-h", "--help"):
308 print(__doc__, end=' ')
309 return 0
310 setup = "\n".join(setup) or "pass"
311
312 # Include the current directory, so that local imports work (sys.path
313 # contains the directory of this script, rather than the current
314 # directory)
315 import os
316 sys.path.insert(0, os.curdir)
317 if _wrap_timer is not None:
318 timer = _wrap_timer(timer)
319
320 t = Timer(stmt, setup, timer)
321 if number == 0:
322 # determine number so that 0.2 <= total time < 2.0
323 callback = None
324 if verbose:
325 def callback(number, time_taken):
326 msg = "{num} loop{s} -> {secs:.{prec}g} secs"
327 plural = (number != 1)
328 print(msg.format(num=number, s='s' if plural else '',
329 secs=time_taken, prec=precision))
330 try:
331 number, _ = t.autorange(callback)
332 except:
333 t.print_exc()
334 return 1
335
336 if verbose:
337 print()
338
339 try:
340 raw_timings = t.repeat(repeat, number)
341 except:
342 t.print_exc()
343 return 1
344
345 def format_time(dt):
346 unit = time_unit
347
348 if unit is not None:
349 scale = units[unit]
350 else:
351 scales = [(scale, unit) for unit, scale in units.items()]
352 scales.sort(reverse=True)
353 for scale, unit in scales:
354 if dt >= scale:
355 break
356
357 return "%.*g %s" % (precision, dt / scale, unit)
358
359 if verbose:
360 print("raw times: %s" % ", ".join(map(format_time, raw_timings)))
361 print()
362 timings = [dt / number for dt in raw_timings]
363
364 best = min(timings)
365 print("%d loop%s, best of %d: %s per loop"
366 % (number, 's' if number != 1 else '',
367 repeat, format_time(best)))
368
369 best = min(timings)
370 worst = max(timings)
371 if worst >= best * 4:
372 import warnings
373 warnings.warn_explicit("The test results are likely unreliable. "
374 "The worst time (%s) was more than four times "
375 "slower than the best time (%s)."
376 % (format_time(worst), format_time(best)),
377 UserWarning, '', 0)
378 return None
379
380
381 if __name__ == "__main__":
382 sys.exit(main())