1#!/usr/local/bin/python3.11 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 pass 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 5; 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 increasing numbers from the sequence 214 1, 2, 5, 10, 20, 50, ... until the time taken is at least 0.2 215 second. Returns (number, time_taken). 216 217 If *callback* is given and is not None, it will be called after 218 each trial with two arguments: ``callback(number, time_taken)``. 219 """ 220 i = 1 221 while True: 222 for j in 1, 2, 5: 223 number = i * j 224 time_taken = self.timeit(number) 225 if callback: 226 callback(number, time_taken) 227 if time_taken >= 0.2: 228 return (number, time_taken) 229 i *= 10 230 231def timeit(stmt="pass", setup="pass", timer=default_timer, 232 number=default_number, globals=None): 233 """Convenience function to create Timer object and call timeit method.""" 234 return Timer(stmt, setup, timer, globals).timeit(number) 235 236def repeat(stmt="pass", setup="pass", timer=default_timer, 237 repeat=default_repeat, number=default_number, globals=None): 238 """Convenience function to create Timer object and call repeat method.""" 239 return Timer(stmt, setup, timer, globals).repeat(repeat, number) 240 241def main(args=None, *, _wrap_timer=None): 242 """Main program, used when run as a script. 243 244 The optional 'args' argument specifies the command line to be parsed, 245 defaulting to sys.argv[1:]. 246 247 The return value is an exit code to be passed to sys.exit(); it 248 may be None to indicate success. 249 250 When an exception happens during timing, a traceback is printed to 251 stderr and the return value is 1. Exceptions at other times 252 (including the template compilation) are not caught. 253 254 '_wrap_timer' is an internal interface used for unit testing. If it 255 is not None, it must be a callable that accepts a timer function 256 and returns another timer function (used for unit testing). 257 """ 258 if args is None: 259 args = sys.argv[1:] 260 import getopt 261 try: 262 opts, args = getopt.getopt(args, "n:u:s:r:tcpvh", 263 ["number=", "setup=", "repeat=", 264 "time", "clock", "process", 265 "verbose", "unit=", "help"]) 266 except getopt.error as err: 267 print(err) 268 print("use -h/--help for command line help") 269 return 2 270 271 timer = default_timer 272 stmt = "\n".join(args) or "pass" 273 number = 0 # auto-determine 274 setup = [] 275 repeat = default_repeat 276 verbose = 0 277 time_unit = None 278 units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0} 279 precision = 3 280 for o, a in opts: 281 if o in ("-n", "--number"): 282 number = int(a) 283 if o in ("-s", "--setup"): 284 setup.append(a) 285 if o in ("-u", "--unit"): 286 if a in units: 287 time_unit = a 288 else: 289 print("Unrecognized unit. Please select nsec, usec, msec, or sec.", 290 file=sys.stderr) 291 return 2 292 if o in ("-r", "--repeat"): 293 repeat = int(a) 294 if repeat <= 0: 295 repeat = 1 296 if o in ("-p", "--process"): 297 timer = time.process_time 298 if o in ("-v", "--verbose"): 299 if verbose: 300 precision += 1 301 verbose += 1 302 if o in ("-h", "--help"): 303 print(__doc__, end=' ') 304 return 0 305 setup = "\n".join(setup) or "pass" 306 307 # Include the current directory, so that local imports work (sys.path 308 # contains the directory of this script, rather than the current 309 # directory) 310 import os 311 sys.path.insert(0, os.curdir) 312 if _wrap_timer is not None: 313 timer = _wrap_timer(timer) 314 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} loop{s} -> {secs:.{prec}g} secs" 322 plural = (number != 1) 323 print(msg.format(num=number, s='s' if plural else '', 324 secs=time_taken, prec=precision)) 325 try: 326 number, _ = t.autorange(callback) 327 except: 328 t.print_exc() 329 return 1 330 331 if verbose: 332 print() 333 334 try: 335 raw_timings = t.repeat(repeat, number) 336 except: 337 t.print_exc() 338 return 1 339 340 def format_time(dt): 341 unit = time_unit 342 343 if unit is not None: 344 scale = units[unit] 345 else: 346 scales = [(scale, unit) for unit, scale in units.items()] 347 scales.sort(reverse=True) 348 for scale, unit in scales: 349 if dt >= scale: 350 break 351 352 return "%.*g %s" % (precision, dt / scale, unit) 353 354 if verbose: 355 print("raw times: %s" % ", ".join(map(format_time, raw_timings))) 356 print() 357 timings = [dt / number for dt in raw_timings] 358 359 best = min(timings) 360 print("%d loop%s, best of %d: %s per loop" 361 % (number, 's' if number != 1 else '', 362 repeat, format_time(best))) 363 364 best = min(timings) 365 worst = max(timings) 366 if worst >= best * 4: 367 import warnings 368 warnings.warn_explicit("The test results are likely unreliable. " 369 "The worst time (%s) was more than four times " 370 "slower than the best time (%s)." 371 % (format_time(worst), format_time(best)), 372 UserWarning, '', 0) 373 return None 374 375if __name__ == "__main__": 376 sys.exit(main()) 377