#!/usr/bin/env python # MIT License # # Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ A script for timing snippets of Python code. By default, this script will execute a single Python file specified on the command line and time any functions in a list named "FunctionList" set by the Python file under test, or (by default) time any functions in the file whose names begin with "Func". All functions are assumed to get passed the same arguments, and the inputs are specified in a list named "Data," each element of which is a list consisting of a tag name, a list of positional arguments, and a dictionary of keyword arguments. Each function is expected to test a single, comparable snippet of of Python code. IMPORTANT: We want to test the timing of the code itself, not Python function call overhead, so every function should put its code under test within the following block: for i in IterationList: This will allow (as much as possible) us to time just the code itself, not Python function call overhead. """ from __future__ import division, print_function import getopt import os.path import sys import time import types from pprint import pprint Usage = """\ Usage: bench.py OPTIONS file.py --clock Use the time.clock function --func PREFIX Test functions whose names begin with PREFIX -h, --help Display this help and exit -i ITER, --iterations ITER Run each code snippet ITER times --time Use the time.time function -r RUNS, --runs RUNS Average times for RUNS invocations of """ # How many times each snippet of code will be (or should be) run by the # functions under test to gather the time (the "inner loop"). Iterations = 1000 # How many times we'll run each function to collect its aggregate time # and try to average out timing differences induced by system performance # (the "outer loop"). Runs = 10 # The prefix of the functions under test. This will be used if # there's no explicit list defined in FunctionList. FunctionPrefix = 'Func' # Comment from Python2 timeit module: # The difference in default timer function is because on Windows, # clock() has microsecond granularity but time()'s granularity is 1/60th # of a second; on Unix, clock() has 1/100th of a second granularity and # time() is much more precise. On either platform, the default timer # functions measure wall clock time, not the CPU time. This means that # other processes running on the same computer may interfere with the # timing. The best thing to do when accurate timing is necessary is to # repeat the timing a few times and use the best time. The -i option is # good for this. # On Python3, a new time.perf_counter function picks the best available # timer, so we use that if we can, else fall back as per above. try: Now = time.perf_counter except AttributeError: if sys.platform == 'win32': Now = time.clock else: Now = time.time opts, args = getopt.getopt(sys.argv[1:], 'hi:r:', ['clock', 'func=', 'help', 'iterations=', 'time', 'runs=']) for o, a in opts: if o in ['--clock']: try: Now = time.clock except AttributeError: sys.stderr.write("time.clock unavailable on this Python\n") sys.exit(1) elif o in ['--func']: FunctionPrefix = a elif o in ['-h', '--help']: sys.stdout.write(Usage) sys.exit(0) elif o in ['-i', '--iterations']: Iterations = int(a) elif o in ['--time']: Now = time.time elif o in ['-r', '--runs']: Runs = int(a) if len(args) != 1: sys.stderr.write("bench.py: only one file argument must be specified\n") sys.stderr.write(Usage) sys.exit(1) (bench_name,_) = os.path.splitext(args[0]) print(f"Bench: {bench_name} : {args[0]}") results_filename = f"{bench_name}_results.csv" with open(args[0], 'r') as f: exec(f.read()) try: FunctionList except NameError: function_names = sorted([x for x in list(locals().keys()) if x[:4] == FunctionPrefix]) lvars = locals() l = [lvars[f] for f in function_names] FunctionList = [f for f in l if isinstance(f, types.FunctionType)] IterationList = [None] * Iterations def timer(func, *args, **kw): results = [] for i in range(Runs): start = Now() func(*args, **kw) finish = Now() results.append((finish - start) / Iterations) return results results_dict = {} def display(func, result_label, results): total = 0.0 for r in results: total += r test_result = (total * 1e6) / len(results) print(" %8.3f" % (test_result), ':', result_label) if results_dict.get(func.__doc__, False): results_dict[func.__doc__][result_label] = test_result else: results_dict[func.__doc__] = { result_label: test_result} for func in FunctionList: if func.__doc__: d = ' (' + func.__doc__ + ')' else: d = '' print(func.__name__ + d + ':') for label, args, kw in Data: r = timer(func, *args, **kw) # print(f"{label} {r}") display(func, label, r) py_ver_string = "%d.%d"%(sys.version_info.major, sys.version_info.minor) # pprint(results_dict) # breakpoint() tests = [label for label, args, kw in Data] columns = ['Python Version', 'Implementation', 'Test'] + tests with open(results_filename, 'a') as r: print("Python Version,%s" % ".".join(columns), file=r) # print("Python Version,%s" % ".".join(columns)) for implementation in results_dict.keys(): for test in tests: print(f'{py_ver_string},"{implementation}","{test}",%8.3f' % results_dict[implementation][test], file=r) # print(f'{py_ver_string},"{implementation}","{test}",%8.3f' % results_dict[implementation][test]) # Local Variables: # tab-width:4 # indent-tabs-mode:nil # End: # vim: set expandtab tabstop=4 shiftwidth=4: