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