mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 18:28:49 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			343 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			343 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #! /usr/bin/env python3
 | |
| 
 | |
| """Tool for measuring execution time of small code snippets.
 | |
| 
 | |
| This module avoids a number of common traps for measuring execution
 | |
| times.  See also Tim Peters' introduction to the Algorithms chapter in
 | |
| the Python Cookbook, published by O'Reilly.
 | |
| 
 | |
| Library usage: see the Timer class.
 | |
| 
 | |
| Command line usage:
 | |
|     python timeit.py [-n N] [-r N] [-s S] [-t] [-c] [-p] [-h] [--] [statement]
 | |
| 
 | |
| Options:
 | |
|   -n/--number N: how many times to execute 'statement' (default: see below)
 | |
|   -r/--repeat N: how many times to repeat the timer (default 3)
 | |
|   -s/--setup S: statement to be executed once initially (default 'pass').
 | |
|                 Execution time of this setup statement is NOT timed.
 | |
|   -p/--process: use time.process_time() (default is time.perf_counter())
 | |
|   -t/--time: use time.time() (deprecated)
 | |
|   -c/--clock: use time.clock() (deprecated)
 | |
|   -v/--verbose: print raw timing results; repeat for more digits precision
 | |
|   -u/--unit: set the output time unit (usec, msec, or sec)
 | |
|   -h/--help: print this usage message and exit
 | |
|   --: separate options from statement, use when statement starts with -
 | |
|   statement: statement to be timed (default 'pass')
 | |
| 
 | |
| A multi-line statement may be given by specifying each line as a
 | |
| separate argument; indented lines are possible by enclosing an
 | |
| argument in quotes and using leading spaces.  Multiple -s options are
 | |
| treated similarly.
 | |
| 
 | |
| If -n is not given, a suitable number of loops is calculated by trying
 | |
| successive powers of 10 until the total time is at least 0.2 seconds.
 | |
| 
 | |
| Note: there is a certain baseline overhead associated with executing a
 | |
| pass statement.  It differs between versions.  The code here doesn't try
 | |
| to hide it, but you should be aware of it.  The baseline overhead can be
 | |
| measured by invoking the program without arguments.
 | |
| 
 | |
| Classes:
 | |
| 
 | |
|     Timer
 | |
| 
 | |
| Functions:
 | |
| 
 | |
|     timeit(string, string) -> float
 | |
|     repeat(string, string) -> list
 | |
|     default_timer() -> float
 | |
| 
 | |
| """
 | |
| 
 | |
| import gc
 | |
| import sys
 | |
| import time
 | |
| import itertools
 | |
| 
 | |
| __all__ = ["Timer", "timeit", "repeat", "default_timer"]
 | |
| 
 | |
| dummy_src_name = "<timeit-src>"
 | |
| default_number = 1000000
 | |
| default_repeat = 3
 | |
| default_timer = time.perf_counter
 | |
| 
 | |
| _globals = globals
 | |
| 
 | |
| # Don't change the indentation of the template; the reindent() calls
 | |
| # in Timer.__init__() depend on setup being indented 4 spaces and stmt
 | |
| # being indented 8 spaces.
 | |
| template = """
 | |
| def inner(_it, _timer{init}):
 | |
|     {setup}
 | |
|     _t0 = _timer()
 | |
|     for _i in _it:
 | |
|         {stmt}
 | |
|     _t1 = _timer()
 | |
|     return _t1 - _t0
 | |
| """
 | |
| 
 | |
| def reindent(src, indent):
 | |
|     """Helper to reindent a multi-line statement."""
 | |
|     return src.replace("\n", "\n" + " "*indent)
 | |
| 
 | |
| class Timer:
 | |
|     """Class for timing execution speed of small code snippets.
 | |
| 
 | |
|     The constructor takes a statement to be timed, an additional
 | |
|     statement used for setup, and a timer function.  Both statements
 | |
|     default to 'pass'; the timer function is platform-dependent (see
 | |
|     module doc string).  If 'globals' is specified, the code will be
 | |
|     executed within that namespace (as opposed to inside timeit's
 | |
|     namespace).
 | |
| 
 | |
|     To measure the execution time of the first statement, use the
 | |
|     timeit() method.  The repeat() method is a convenience to call
 | |
|     timeit() multiple times and return a list of results.
 | |
| 
 | |
|     The statements may contain newlines, as long as they don't contain
 | |
|     multi-line string literals.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, stmt="pass", setup="pass", timer=default_timer,
 | |
|                  globals=None):
 | |
|         """Constructor.  See class doc string."""
 | |
|         self.timer = timer
 | |
|         local_ns = {}
 | |
|         global_ns = _globals() if globals is None else globals
 | |
|         init = ''
 | |
|         if isinstance(setup, str):
 | |
|             # Check that the code can be compiled outside a function
 | |
|             compile(setup, dummy_src_name, "exec")
 | |
|             stmtprefix = setup + '\n'
 | |
|             setup = reindent(setup, 4)
 | |
|         elif callable(setup):
 | |
|             local_ns['_setup'] = setup
 | |
|             init += ', _setup=_setup'
 | |
|             stmtprefix = ''
 | |
|             setup = '_setup()'
 | |
|         else:
 | |
|             raise ValueError("setup is neither a string nor callable")
 | |
|         if isinstance(stmt, str):
 | |
|             # Check that the code can be compiled outside a function
 | |
|             compile(stmtprefix + stmt, dummy_src_name, "exec")
 | |
|             stmt = reindent(stmt, 8)
 | |
|         elif callable(stmt):
 | |
|             local_ns['_stmt'] = stmt
 | |
|             init += ', _stmt=_stmt'
 | |
|             stmt = '_stmt()'
 | |
|         else:
 | |
|             raise ValueError("stmt is neither a string nor callable")
 | |
|         src = template.format(stmt=stmt, setup=setup, init=init)
 | |
|         self.src = src  # Save for traceback display
 | |
|         code = compile(src, dummy_src_name, "exec")
 | |
|         exec(code, global_ns, local_ns)
 | |
|         self.inner = local_ns["inner"]
 | |
| 
 | |
|     def print_exc(self, file=None):
 | |
|         """Helper to print a traceback from the timed code.
 | |
| 
 | |
|         Typical use:
 | |
| 
 | |
|             t = Timer(...)       # outside the try/except
 | |
|             try:
 | |
|                 t.timeit(...)    # or t.repeat(...)
 | |
|             except:
 | |
|                 t.print_exc()
 | |
| 
 | |
|         The advantage over the standard traceback is that source lines
 | |
|         in the compiled template will be displayed.
 | |
| 
 | |
|         The optional file argument directs where the traceback is
 | |
|         sent; it defaults to sys.stderr.
 | |
|         """
 | |
|         import linecache, traceback
 | |
|         if self.src is not None:
 | |
|             linecache.cache[dummy_src_name] = (len(self.src),
 | |
|                                                None,
 | |
|                                                self.src.split("\n"),
 | |
|                                                dummy_src_name)
 | |
|         # else the source is already stored somewhere else
 | |
| 
 | |
|         traceback.print_exc(file=file)
 | |
| 
 | |
|     def timeit(self, number=default_number):
 | |
|         """Time 'number' executions of the main statement.
 | |
| 
 | |
|         To be precise, this executes the setup statement once, and
 | |
|         then returns the time it takes to execute the main statement
 | |
|         a number of times, as a float measured in seconds.  The
 | |
|         argument is the number of times through the loop, defaulting
 | |
|         to one million.  The main statement, the setup statement and
 | |
|         the timer function to be used are passed to the constructor.
 | |
|         """
 | |
|         it = itertools.repeat(None, number)
 | |
|         gcold = gc.isenabled()
 | |
|         gc.disable()
 | |
|         try:
 | |
|             timing = self.inner(it, self.timer)
 | |
|         finally:
 | |
|             if gcold:
 | |
|                 gc.enable()
 | |
|         return timing
 | |
| 
 | |
|     def repeat(self, repeat=default_repeat, number=default_number):
 | |
|         """Call timeit() a few times.
 | |
| 
 | |
|         This is a convenience function that calls the timeit()
 | |
|         repeatedly, returning a list of results.  The first argument
 | |
|         specifies how many times to call timeit(), defaulting to 3;
 | |
|         the second argument specifies the timer argument, defaulting
 | |
|         to one million.
 | |
| 
 | |
|         Note: it's tempting to calculate mean and standard deviation
 | |
|         from the result vector and report these.  However, this is not
 | |
|         very useful.  In a typical case, the lowest value gives a
 | |
|         lower bound for how fast your machine can run the given code
 | |
|         snippet; higher values in the result vector are typically not
 | |
|         caused by variability in Python's speed, but by other
 | |
|         processes interfering with your timing accuracy.  So the min()
 | |
|         of the result is probably the only number you should be
 | |
|         interested in.  After that, you should look at the entire
 | |
|         vector and apply common sense rather than statistics.
 | |
|         """
 | |
|         r = []
 | |
|         for i in range(repeat):
 | |
|             t = self.timeit(number)
 | |
|             r.append(t)
 | |
|         return r
 | |
| 
 | |
| def timeit(stmt="pass", setup="pass", timer=default_timer,
 | |
|            number=default_number, globals=None):
 | |
|     """Convenience function to create Timer object and call timeit method."""
 | |
|     return Timer(stmt, setup, timer, globals).timeit(number)
 | |
| 
 | |
| def repeat(stmt="pass", setup="pass", timer=default_timer,
 | |
|            repeat=default_repeat, number=default_number, globals=None):
 | |
|     """Convenience function to create Timer object and call repeat method."""
 | |
|     return Timer(stmt, setup, timer, globals).repeat(repeat, number)
 | |
| 
 | |
| def main(args=None, *, _wrap_timer=None):
 | |
|     """Main program, used when run as a script.
 | |
| 
 | |
|     The optional 'args' argument specifies the command line to be parsed,
 | |
|     defaulting to sys.argv[1:].
 | |
| 
 | |
|     The return value is an exit code to be passed to sys.exit(); it
 | |
|     may be None to indicate success.
 | |
| 
 | |
|     When an exception happens during timing, a traceback is printed to
 | |
|     stderr and the return value is 1.  Exceptions at other times
 | |
|     (including the template compilation) are not caught.
 | |
| 
 | |
|     '_wrap_timer' is an internal interface used for unit testing.  If it
 | |
|     is not None, it must be a callable that accepts a timer function
 | |
|     and returns another timer function (used for unit testing).
 | |
|     """
 | |
|     if args is None:
 | |
|         args = sys.argv[1:]
 | |
|     import getopt
 | |
|     try:
 | |
|         opts, args = getopt.getopt(args, "n:u:s:r:tcpvh",
 | |
|                                    ["number=", "setup=", "repeat=",
 | |
|                                     "time", "clock", "process",
 | |
|                                     "verbose", "unit=", "help"])
 | |
|     except getopt.error as err:
 | |
|         print(err)
 | |
|         print("use -h/--help for command line help")
 | |
|         return 2
 | |
|     timer = default_timer
 | |
|     stmt = "\n".join(args) or "pass"
 | |
|     number = 0 # auto-determine
 | |
|     setup = []
 | |
|     repeat = default_repeat
 | |
|     verbose = 0
 | |
|     time_unit = None
 | |
|     units = {"usec": 1, "msec": 1e3, "sec": 1e6}
 | |
|     precision = 3
 | |
|     for o, a in opts:
 | |
|         if o in ("-n", "--number"):
 | |
|             number = int(a)
 | |
|         if o in ("-s", "--setup"):
 | |
|             setup.append(a)
 | |
|         if o in ("-u", "--unit"):
 | |
|             if a in units:
 | |
|                 time_unit = a
 | |
|             else:
 | |
|                 print("Unrecognized unit. Please select usec, msec, or sec.",
 | |
|                     file=sys.stderr)
 | |
|                 return 2
 | |
|         if o in ("-r", "--repeat"):
 | |
|             repeat = int(a)
 | |
|             if repeat <= 0:
 | |
|                 repeat = 1
 | |
|         if o in ("-t", "--time"):
 | |
|             timer = time.time
 | |
|         if o in ("-c", "--clock"):
 | |
|             timer = time.clock
 | |
|         if o in ("-p", "--process"):
 | |
|             timer = time.process_time
 | |
|         if o in ("-v", "--verbose"):
 | |
|             if verbose:
 | |
|                 precision += 1
 | |
|             verbose += 1
 | |
|         if o in ("-h", "--help"):
 | |
|             print(__doc__, end=' ')
 | |
|             return 0
 | |
|     setup = "\n".join(setup) or "pass"
 | |
|     # Include the current directory, so that local imports work (sys.path
 | |
|     # contains the directory of this script, rather than the current
 | |
|     # directory)
 | |
|     import os
 | |
|     sys.path.insert(0, os.curdir)
 | |
|     if _wrap_timer is not None:
 | |
|         timer = _wrap_timer(timer)
 | |
|     t = Timer(stmt, setup, timer)
 | |
|     if number == 0:
 | |
|         # determine number so that 0.2 <= total time < 2.0
 | |
|         for i in range(1, 10):
 | |
|             number = 10**i
 | |
|             try:
 | |
|                 x = t.timeit(number)
 | |
|             except:
 | |
|                 t.print_exc()
 | |
|                 return 1
 | |
|             if verbose:
 | |
|                 print("%d loops -> %.*g secs" % (number, precision, x))
 | |
|             if x >= 0.2:
 | |
|                 break
 | |
|     try:
 | |
|         r = t.repeat(repeat, number)
 | |
|     except:
 | |
|         t.print_exc()
 | |
|         return 1
 | |
|     best = min(r)
 | |
|     if verbose:
 | |
|         print("raw times:", " ".join(["%.*g" % (precision, x) for x in r]))
 | |
|     print("%d loops," % number, end=' ')
 | |
|     usec = best * 1e6 / number
 | |
|     if time_unit is not None:
 | |
|         scale = units[time_unit]
 | |
|     else:
 | |
|         scales = [(scale, unit) for unit, scale in units.items()]
 | |
|         scales.sort(reverse=True)
 | |
|         for scale, time_unit in scales:
 | |
|             if usec >= scale:
 | |
|                 break
 | |
|     print("best of %d: %.*g %s per loop" % (repeat, precision,
 | |
|                                             usec/scale, time_unit))
 | |
|     best = min(r)
 | |
|     usec = best * 1e6 / number
 | |
|     worst = max(r)
 | |
|     if worst >= best * 4:
 | |
|         usec = worst * 1e6 / number
 | |
|         import warnings
 | |
|         warnings.warn_explicit(
 | |
|             "The test results are likely unreliable. The worst\n"
 | |
|             "time (%.*g %s) was more than four times slower than the best time." %
 | |
|             (precision, usec/scale, time_unit),
 | |
|              UserWarning, '', 0)
 | |
|     return None
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     sys.exit(main())
 | 
