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] [-t] [-c] [-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 3) 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 -t/--time: use time.time() (deprecated) 21 -c/--clock: use time.clock() (deprecated) 22 -v/--verbose: print raw timing results; repeat for more digits precision 23 -u/--unit: set the output time unit (usec, msec, or sec) 24 -h/--help: print this usage message and exit 25 --: separate options from statement, use when statement starts with - 26 statement: statement to be timed (default 'pass') 27 28A multi-line statement may be given by specifying each line as a 29separate argument; indented lines are possible by enclosing an 30argument in quotes and using leading spaces. Multiple -s options are 31treated similarly. 32 33If -n is not given, a suitable number of loops is calculated by trying 34successive powers of 10 until the total time is at least 0.2 seconds. 35 36Note: there is a certain baseline overhead associated with executing a 37pass statement. It differs between versions. The code here doesn't try 38to hide it, but you should be aware of it. The baseline overhead can be 39measured by invoking the program without arguments. 40 41Classes: 42 43 Timer 44 45Functions: 46 47 timeit(string, string) -> float 48 repeat(string, string) -> list 49 default_timer() -> float 50 51""" 52 53import gc 54import sys 55import time 56import itertools 57 58__all__ = ["Timer", "timeit", "repeat", "default_timer"] 59 60dummy_src_name = "<timeit-src>" 61default_number = 1000000 62default_repeat = 3 63default_timer = time.perf_counter 64 65_globals = globals 66 67# Don't change the indentation of the template; the reindent() calls 68# in Timer.__init__() depend on setup being indented 4 spaces and stmt 69# being indented 8 spaces. 70template = """ 71def inner(_it, _timer{init}): 72 {setup} 73 _t0 = _timer() 74 for _i in _it: 75 {stmt} 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 3; 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 *number* set to successive powers of 214 ten (10, 100, 1000, ...) up to a maximum of one billion, until 215 the time taken is at least 0.2 second, or the maximum is reached. 216 Returns ``(number, time_taken)``. 217 218 If *callback* is given and is not None, it will be called after 219 each trial with two arguments: ``callback(number, time_taken)``. 220 """ 221 for i in range(1, 10): 222 number = 10**i 223 time_taken = self.timeit(number) 224 if callback: 225 callback(number, time_taken) 226 if time_taken >= 0.2: 227 break 228 return (number, time_taken) 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 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 = {"usec": 1, "msec": 1e3, "sec": 1e6} 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 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 ("-t", "--time"): 295 timer = time.time 296 if o in ("-c", "--clock"): 297 timer = time.clock 298 if o in ("-p", "--process"): 299 timer = time.process_time 300 if o in ("-v", "--verbose"): 301 if verbose: 302 precision += 1 303 verbose += 1 304 if o in ("-h", "--help"): 305 print(__doc__, end=' ') 306 return 0 307 setup = "\n".join(setup) or "pass" 308 # Include the current directory, so that local imports work (sys.path 309 # contains the directory of this script, rather than the current 310 # directory) 311 import os 312 sys.path.insert(0, os.curdir) 313 if _wrap_timer is not None: 314 timer = _wrap_timer(timer) 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} loops -> {secs:.{prec}g} secs" 322 print(msg.format(num=number, secs=time_taken, prec=precision)) 323 try: 324 number, _ = t.autorange(callback) 325 except: 326 t.print_exc() 327 return 1 328 try: 329 r = t.repeat(repeat, number) 330 except: 331 t.print_exc() 332 return 1 333 best = min(r) 334 if verbose: 335 print("raw times:", " ".join(["%.*g" % (precision, x) for x in r])) 336 print("%d loops," % number, end=' ') 337 usec = best * 1e6 / number 338 if time_unit is not None: 339 scale = units[time_unit] 340 else: 341 scales = [(scale, unit) for unit, scale in units.items()] 342 scales.sort(reverse=True) 343 for scale, time_unit in scales: 344 if usec >= scale: 345 break 346 print("best of %d: %.*g %s per loop" % (repeat, precision, 347 usec/scale, time_unit)) 348 best = min(r) 349 usec = best * 1e6 / number 350 worst = max(r) 351 if worst >= best * 4: 352 usec = worst * 1e6 / number 353 import warnings 354 warnings.warn_explicit( 355 "The test results are likely unreliable. The worst\n" 356 "time (%.*g %s) was more than four times slower than the best time." % 357 (precision, usec/scale, time_unit), 358 UserWarning, '', 0) 359 return None 360 361if __name__ == "__main__": 362 sys.exit(main()) 363