codekingpro/portable-devtools
114k
1"""Tool for measuring execution time of small code snippets.2 3This module avoids a number of common traps for measuring execution4times. See also Tim Peters' introduction to the Algorithms chapter in5the Python Cookbook, published by O'Reilly.6 7Library usage: see the Timer class.8 9Command line usage:10 python timeit.py [-n N] [-r N] [-s S] [-p] [-h] [--] [statement]11 12Options:13 -n/--number N: how many times to execute 'statement' (default: see below)14 -r/--repeat N: how many times to repeat the timer (default 5)15 -s/--setup S: statement to be executed once initially (default 'pass').16 Execution time of this setup statement is NOT timed.17 -p/--process: use time.process_time() (default is time.perf_counter())18 -v/--verbose: print raw timing results; repeat for more digits precision19 -u/--unit: set the output time unit (nsec, usec, msec, or sec)20 -h/--help: print this usage message and exit21 --: separate options from statement, use when statement starts with -22 statement: statement to be timed (default 'pass')23 24A multi-line statement may be given by specifying each line as a25separate argument; indented lines are possible by enclosing an26argument in quotes and using leading spaces. Multiple -s options are27treated similarly.28 29If -n is not given, a suitable number of loops is calculated by trying30increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the31total time is at least 0.2 seconds.32 33Note: there is a certain baseline overhead associated with executing a34pass statement. It differs between versions. The code here doesn't try35to hide it, but you should be aware of it. The baseline overhead can be36measured by invoking the program without arguments.37 38Classes:39 40 Timer41 42Functions:43 44 timeit(string, string) -> float45 repeat(string, string) -> list46 default_timer() -> float47"""48 49import gc50import itertools51import sys52import time53 54__all__ = ["Timer", "timeit", "repeat", "default_timer"]55 56dummy_src_name = "<timeit-src>"57default_number = 100000058default_repeat = 559default_timer = time.perf_counter60 61_globals = globals62 63# Don't change the indentation of the template; the reindent() calls64# in Timer.__init__() depend on setup being indented 4 spaces and stmt65# being indented 8 spaces.66template = """67def inner(_it, _timer{init}):68 {setup}69 _t0 = _timer()70 for _i in _it:71 {stmt}72 pass73 _t1 = _timer()74 return _t1 - _t075"""76 77 78def reindent(src, indent):79 """Helper to reindent a multi-line statement."""80 return src.replace("\n", "\n" + " " * indent)81 82 83class Timer:84 """Class for timing execution speed of small code snippets.85 86 The constructor takes a statement to be timed, an additional87 statement used for setup, and a timer function. Both statements88 default to 'pass'; the timer function is platform-dependent (see89 module doc string). If 'globals' is specified, the code will be90 executed within that namespace (as opposed to inside timeit's91 namespace).92 93 To measure the execution time of the first statement, use the94 timeit() method. The repeat() method is a convenience to call95 timeit() multiple times and return a list of results.96 97 The statements may contain newlines, as long as they don't contain98 multi-line string literals.99 """100 101 def __init__(self, stmt="pass", setup="pass", timer=default_timer,102 globals=None):103 """Constructor. See class doc string."""104 self.timer = timer105 local_ns = {}106 global_ns = _globals() if globals is None else globals107 init = ''108 if isinstance(setup, str):109 # Check that the code can be compiled outside a function110 compile(setup, dummy_src_name, "exec")111 stmtprefix = setup + '\n'112 setup = reindent(setup, 4)113 elif callable(setup):114 local_ns['_setup'] = setup115 init += ', _setup=_setup'116 stmtprefix = ''117 setup = '_setup()'118 else:119 raise ValueError("setup is neither a string nor callable")120 if isinstance(stmt, str):121 # Check that the code can be compiled outside a function122 compile(stmtprefix + stmt, dummy_src_name, "exec")123 stmt = reindent(stmt, 8)124 elif callable(stmt):125 local_ns['_stmt'] = stmt126 init += ', _stmt=_stmt'127 stmt = '_stmt()'128 else:129 raise ValueError("stmt is neither a string nor callable")130 src = template.format(stmt=stmt, setup=setup, init=init)131 self.src = src # Save for traceback display132 code = compile(src, dummy_src_name, "exec")133 exec(code, global_ns, local_ns)134 self.inner = local_ns["inner"]135 136 def print_exc(self, file=None):137 """Helper to print a traceback from the timed code.138 139 Typical use:140 141 t = Timer(...) # outside the try/except142 try:143 t.timeit(...) # or t.repeat(...)144 except:145 t.print_exc()146 147 The advantage over the standard traceback is that source lines148 in the compiled template will be displayed.149 150 The optional file argument directs where the traceback is151 sent; it defaults to sys.stderr.152 """153 import linecache, traceback154 if self.src is not None:155 linecache.cache[dummy_src_name] = (len(self.src),156 None,157 self.src.split("\n"),158 dummy_src_name)159 # else the source is already stored somewhere else160 161 traceback.print_exc(file=file)162 163 def timeit(self, number=default_number):164 """Time 'number' executions of the main statement.165 166 To be precise, this executes the setup statement once, and167 then returns the time it takes to execute the main statement168 a number of times, as float seconds if using the default timer. The169 argument is the number of times through the loop, defaulting170 to one million. The main statement, the setup statement and171 the timer function to be used are passed to the constructor.172 """173 it = itertools.repeat(None, number)174 gcold = gc.isenabled()175 gc.disable()176 try:177 timing = self.inner(it, self.timer)178 finally:179 if gcold:180 gc.enable()181 return timing182 183 def repeat(self, repeat=default_repeat, number=default_number):184 """Call timeit() a few times.185 186 This is a convenience function that calls the timeit()187 repeatedly, returning a list of results. The first argument188 specifies how many times to call timeit(), defaulting to 5;189 the second argument specifies the timer argument, defaulting190 to one million.191 192 Note: it's tempting to calculate mean and standard deviation193 from the result vector and report these. However, this is not194 very useful. In a typical case, the lowest value gives a195 lower bound for how fast your machine can run the given code196 snippet; higher values in the result vector are typically not197 caused by variability in Python's speed, but by other198 processes interfering with your timing accuracy. So the min()199 of the result is probably the only number you should be200 interested in. After that, you should look at the entire201 vector and apply common sense rather than statistics.202 """203 r = []204 for i in range(repeat):205 t = self.timeit(number)206 r.append(t)207 return r208 209 def autorange(self, callback=None):210 """Return the number of loops and time taken so that total time >= 0.2.211 212 Calls the timeit method with increasing numbers from the sequence213 1, 2, 5, 10, 20, 50, ... until the time taken is at least 0.2214 second. Returns (number, time_taken).215 216 If *callback* is given and is not None, it will be called after217 each trial with two arguments: ``callback(number, time_taken)``.218 """219 i = 1220 while True:221 for j in 1, 2, 5:222 number = i * j223 time_taken = self.timeit(number)224 if callback:225 callback(number, time_taken)226 if time_taken >= 0.2:227 return (number, time_taken)228 i *= 10229 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 236 237def repeat(stmt="pass", setup="pass", timer=default_timer,238 repeat=default_repeat, number=default_number, globals=None):239 """Convenience function to create Timer object and call repeat method."""240 return Timer(stmt, setup, timer, globals).repeat(repeat, number)241 242 243def main(args=None, *, _wrap_timer=None):244 """Main program, used when run as a script.245 246 The optional 'args' argument specifies the command line to be parsed,247 defaulting to sys.argv[1:].248 249 The return value is an exit code to be passed to sys.exit(); it250 may be None to indicate success.251 252 When an exception happens during timing, a traceback is printed to253 stderr and the return value is 1. Exceptions at other times254 (including the template compilation) are not caught.255 256 '_wrap_timer' is an internal interface used for unit testing. If it257 is not None, it must be a callable that accepts a timer function258 and returns another timer function (used for unit testing).259 """260 if args is None:261 args = sys.argv[1:]262 import getopt263 try:264 opts, args = getopt.getopt(args, "n:u:s:r:pvh",265 ["number=", "setup=", "repeat=",266 "process", "verbose", "unit=", "help"])267 except getopt.error as err:268 print(err)269 print("use -h/--help for command line help")270 return 2271 272 timer = default_timer273 stmt = "\n".join(args) or "pass"274 number = 0 # auto-determine275 setup = []276 repeat = default_repeat277 verbose = 0278 time_unit = None279 units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0}280 precision = 3281 for o, a in opts:282 if o in ("-n", "--number"):283 number = int(a)284 if o in ("-s", "--setup"):285 setup.append(a)286 if o in ("-u", "--unit"):287 if a in units:288 time_unit = a289 else:290 print("Unrecognized unit. Please select nsec, usec, msec, or sec.",291 file=sys.stderr)292 return 2293 if o in ("-r", "--repeat"):294 repeat = int(a)295 if repeat <= 0:296 repeat = 1297 if o in ("-p", "--process"):298 timer = time.process_time299 if o in ("-v", "--verbose"):300 if verbose:301 precision += 1302 verbose += 1303 if o in ("-h", "--help"):304 print(__doc__, end="")305 return 0306 setup = "\n".join(setup) or "pass"307 308 # Include the current directory, so that local imports work (sys.path309 # contains the directory of this script, rather than the current310 # directory)311 import os312 sys.path.insert(0, os.curdir)313 if _wrap_timer is not None:314 timer = _wrap_timer(timer)315 316 t = Timer(stmt, setup, timer)317 if number == 0:318 # determine number so that 0.2 <= total time < 2.0319 callback = None320 if verbose:321 def callback(number, time_taken):322 msg = "{num} loop{s} -> {secs:.{prec}g} secs"323 plural = (number != 1)324 print(msg.format(num=number, s='s' if plural else '',325 secs=time_taken, prec=precision))326 try:327 number, _ = t.autorange(callback)328 except:329 t.print_exc()330 return 1331 332 if verbose:333 print()334 335 try:336 raw_timings = t.repeat(repeat, number)337 except:338 t.print_exc()339 return 1340 341 def format_time(dt):342 unit = time_unit343 344 if unit is not None:345 scale = units[unit]346 else:347 scales = [(scale, unit) for unit, scale in units.items()]348 scales.sort(reverse=True)349 for scale, unit in scales:350 if dt >= scale:351 break352 353 return "%.*g %s" % (precision, dt / scale, unit)354 355 if verbose:356 print("raw times: %s" % ", ".join(map(format_time, raw_timings)))357 print()358 timings = [dt / number for dt in raw_timings]359 360 best = min(timings)361 print("%d loop%s, best of %d: %s per loop"362 % (number, 's' if number != 1 else '',363 repeat, format_time(best)))364 365 best = min(timings)366 worst = max(timings)367 if worst >= best * 4:368 import warnings369 warnings.warn_explicit("The test results are likely unreliable. "370 "The worst time (%s) was more than four times "371 "slower than the best time (%s)."372 % (format_time(worst), format_time(best)),373 UserWarning, '', 0)374 return None375 376 377if __name__ == "__main__":378 sys.exit(main())379 