# HG changeset patch # User Scott David Daniels # Date 1247069686 25200 # Branch trunk # Node ID 2805d02b871bb022da0d5d5d0d941c31cd576e9b # Parent c7f03e7b8061084c3e0a2bf27b592b79ce68d72b Move the code in the main function used to determine sample times for number = 0 to a new method "timed_reps" on the Timer class. Add use of that new sample time to the "timeit" and "repeat" convenience functions by making number=None use the behavior. diff --git a/Doc/library/timeit.rst b/Doc/library/timeit.rst --- a/Doc/library/timeit.rst +++ b/Doc/library/timeit.rst @@ -93,32 +93,55 @@ The module defines the following public during the timing. The advantage of this approach is that it makes independent timings more comparable. This disadvantage is that GC may be an important component of the performance of the function being measured. If so, GC can be re-enabled as the first statement in the *setup* string. For example:: timeit.Timer('for i in xrange(10): oct(i)', 'gc.enable()').timeit() + +.. method:: Timer.timed_reps(at_least, [precision=None]) + + Time increasing numbers of executions of the main statement via + calls to :meth:`timeit` until the time taken to execute the last group + meets or exceeds *at_least* seconds. This method returns a pair of the + *number* parameter used by that last call to :meth:`timeit` and the + return from that call. + If the *precision* parameter is not None, each use of :meth:`timeit` + is output using g-formatting on the time in seconds with the *precision* + parameter provided to the g-format. + + .. versionadded:: 2.6 + + Starting with version 2.6, the module also defines two convenience functions: .. function:: repeat(stmt[, setup[, timer[, repeat=3 [, number=1000000]]]]) Create a :class:`Timer` instance with the given statement, setup code and timer - function and run its :meth:`repeat` method with the given repeat count and + function and run its :meth:`repeat` method with the given *repeat* count and *number* executions. + If number is passed in as None, a first pass is made estimating a value for + *number* that will produce timings over a tenth of a second, and that value + for *number* is printed and then used. .. versionadded:: 2.6 .. function:: timeit(stmt[, setup[, timer[, number=1000000]]]) - Create a :class:`Timer` instance with the given statement, setup code and timer - function and run its :meth:`timeit` method with *number* executions. + Create a :class:`Timer` instance with the given statement, setup code and + timer function and run its :meth:`timeit` method with *number* executions. + If number is passed in as None, :meth:`timed_reps` method is used to + find a value for *number* that will give timings over a tenth of a second. + In the None case, the value for *number* is printed, but the value + returned will be divided by that and that value for *number* is printed, + but the value returned will be divided by that *number*. .. versionadded:: 2.6 Command Line Interface ---------------------- When called as a program from the command line, the following form is used:: diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py new file mode 100644 --- /dev/null +++ b/Lib/test/test_timeit.py @@ -0,0 +1,157 @@ +import sys +import unittest +import timeit as sut +from StringIO import StringIO + +__version__ = '0.9' + + +class FakeTimedFunction(object): + def __init__(self, timing_per_call, overhead): + self.per_call = timing_per_call + self.overhead = overhead + self.function_counter = 0 + self.wasted = 0 + + def function(self): + self.function_counter += 1 + + def timer(self): + self.wasted += self.overhead + return self.per_call * self.function_counter + self.wasted + + +class TestRepCounts(unittest.TestCase): + + def setUp(self): + self.tf = FakeTimedFunction(.006, 0) + + def tearDown(self): + self.tf = FakeTimedFunction(.006, 0) + + def test_timed_reps_sample(self): + t = sut.Timer(stmt=self.tf.function, setup='pass', timer=self.tf.timer) + reps, elapsed = t.timed_reps(.5) + self.assertEqual(100, reps) + self.assertAlmostEqual(.6, elapsed) + + def test_timed_reps_no_pass(self): + tf = self.tf + tf.per_call = .06 + t = sut.Timer(stmt=tf.function, setup=tf.function, timer=tf.timer) + reps, elapsed = t.timed_reps(.5) + self.assertEqual(10, reps) + self.assertAlmostEqual(.6, elapsed) + + def test_timed_reps_slow(self): + tf = self.tf + tf.per_call = 75.5 + t = sut.Timer(stmt=tf.function, timer=tf.timer) + reps, elapsed = t.timed_reps(.5) + self.assertEqual(1, reps) + self.assertAlmostEqual(75.5, elapsed) + + def test_timed_rep_precision(self): + t = sut.Timer(stmt=self.tf.function, timer=self.tf.timer) + held, sys.stdout = sys.stdout, StringIO() + try: + reps, elapsed = t.timed_reps(.5, precision=3) + finally: + held, sys.stdout = sys.stdout, held + self.assertEqual(held.getvalue(), '1 loops -> 0.006 secs\n' + '10 loops -> 0.06 secs\n' + '100 loops -> 0.6 secs\n') + + def test_timeit_helper(self): + held, sys.stdout = sys.stdout, StringIO() + try: + result = sut.timeit(stmt=self.tf.function, timer=self.tf.timer, + number=100) + finally: + held, sys.stdout = sys.stdout, held + self.assertEqual(held.getvalue(), '') + self.assertAlmostEqual(result, .6) + + def test_timeit_helper_auto(self): + held, sys.stdout = sys.stdout, StringIO() + try: + result = sut.timeit(stmt=self.tf.function, timer=self.tf.timer, + number=None) + finally: + held, sys.stdout = sys.stdout, held + self.assertEqual(held.getvalue(), 'number = 100, divided down.\n') + self.assertAlmostEqual(result, .006) + + def test_repeat_helper(self): + held, sys.stdout = sys.stdout, StringIO() + try: + result = sut.repeat(stmt=self.tf.function, timer=self.tf.timer, + number=10, repeat=2) + finally: + held, sys.stdout = sys.stdout, held + self.assertEqual(held.getvalue(), '') # Says nothing + self.assertEqual(len(result), 2) # two results -- from repeat + self.assertAlmostEqual(sum(result), .12) # expected [0.06, 0.06] + + def test_repeat_helper_auto(self): + held, sys.stdout = sys.stdout, StringIO() + try: + result = sut.repeat(stmt=self.tf.function, timer=self.tf.timer, + number=None, repeat=4) + finally: + held, sys.stdout = sys.stdout, held + self.assertEqual(held.getvalue(), 'number = 100\n') + self.assertAlmostEqual(sum(result), 2.4) + + +class TestCommandLine(unittest.TestCase): + + def test_simple(self): + held, sys.stdout = sys.stdout, StringIO() + try: + sut.main('-n 5 1+2'.split()) + finally: + held, sys.stdout = sys.stdout, held + first, last = held.getvalue().split(':') + self.assertEqual('5 loops, best of %s' % sut.default_repeat, first) + self.assertEqual('usec per loop\n', last.split(None, 1)[1]) + + def test_adaptive(self): + held, sys.stdout = sys.stdout, StringIO() + try: + sut.main(['-s', 'import time', 'time.sleep(0.03)']) + finally: + held, sys.stdout = sys.stdout, held + first, last = held.getvalue().split(':') + self.assertEqual('10 loops, best of %s' % sut.default_repeat, first) + self.assertEqual('msec per loop\n', last.split(None, 1)[1]) + + def test_verbose_adaptive(self): + held, sys.stdout = sys.stdout, StringIO() + try: + sut.main(['-v', '-s', 'import time', 'time.sleep(0.03)']) + finally: + held, sys.stdout = sys.stdout, held + lines = held.getvalue().split('\n') + a, b = lines[0].split(' -> ') + self.assertEqual('1 loops', a) + self.assertEqual('secs', b.split(None, 1)[1]) + a, b = lines[1].split(' -> ') + self.assertEqual('10 loops', a) + self.assertEqual('secs', b.split(None, 1)[1]) + a, b = lines[2].split(':') + self.assertEqual('raw times', a) + self.assertEqual(len(b.split()), sut.default_repeat) + first, last = lines[-2].split(':') + self.assertEqual('10 loops, best of %s' % sut.default_repeat, first) + self.assertEqual('msec per loop', last.split(None, 1)[1]) + self.assertEqual('', lines[-1]) + self.assertEqual(5, len(lines)) + + +if __name__ == '__main__': + #print ('Python v%s' % sys.version) + #print ('Test %s v%s of %s v%s' % ( + # __name__, __version__, sut.__name__, sut.__version__)) + unittest.main() + diff --git a/Lib/timeit.py b/Lib/timeit.py --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -56,16 +56,17 @@ import sys import time try: import itertools except ImportError: # Must be an older Python version (see timeit() below) itertools = None __all__ = ["Timer"] +__version__ = '0.2' dummy_src_name = "" default_number = 1000000 default_repeat = 3 if sys.platform == "win32": # On Windows, the best timer is time.clock() default_timer = time.clock @@ -216,25 +217,53 @@ class Timer: vector and apply common sense rather than statistics. """ r = [] for i in range(repeat): t = self.timeit(number) r.append(t) return r + def timed_reps(self, at_least, precision=None): + '''loops, seconds so .timeit(loops) (10 ** N) takes at_least seconds + + Exceptions are propogated. If precision is None estimate is silent. + If the precision is not None, it is the g-format precision to use + in reporting time at each attempted loop count on the way. + This method tries successive powers of ten til it finds a winner. + ''' + number = 1 # start at 1, not 10, in case first time is "special" + while True: + elapsed = self.timeit(number) + if precision is not None: + print ("%d loops -> %.*g secs" % (number, precision, elapsed)) + if elapsed >= at_least: + return number, elapsed + number *= 10 + + def timeit(stmt="pass", setup="pass", timer=default_timer, number=default_number): """Convenience function to create Timer object and call timeit method.""" - return Timer(stmt, setup, timer).timeit(number) + t = Timer(stmt, setup, timer) + if number is None: + number, elapsed = t.timed_reps(0.11) + print ('number = %s, divided down.' % number) + return elapsed / number + return t.timeit(number) def repeat(stmt="pass", setup="pass", timer=default_timer, repeat=default_repeat, number=default_number): """Convenience function to create Timer object and call repeat method.""" - return Timer(stmt, setup, timer).repeat(repeat, number) + t = Timer(stmt, setup, timer) + if number is None and repeat > 0: + number, elapsed = t.timed_reps(0.11) + print ('number = %s' % number) + return [elapsed] + t.repeat(repeat - 1, number) + return t.repeat(repeat, number) def main(args=None): """Main program, used when run as a script. The optional 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 @@ -286,27 +315,24 @@ def main(args=None): # 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) 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 + try: if verbose: - print "%d loops -> %.*g secs" % (number, precision, x) - if x >= 0.2: - break + number, x = t.timed_reps(0.2, precision) + else: + number, x = t.timed_reps(0.2) + except: + t.print_exc() + return 1 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])