d5e33bb1909553a56ed42c7af5aecc7005fe6e56
[scons.git] / bench / timeit.py
1 #! /usr/bin/env python
2
3 """Tool for measuring execution time of small code snippets.
4
5 This module avoids a number of common traps for measuring execution
6 times.  See also Tim Peters' introduction to the Algorithms chapter in
7 the Python Cookbook, published by O'Reilly.
8
9 Library usage: see the Timer class.
10
11 Command line usage:
12     python timeit.py [-n N] [-r N] [-s S] [-t] [-c] [-h] [statement]
13
14 Options:
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 3)
17   -s/--setup S: statement to be executed once initially (default 'pass')
18   -t/--time: use time.time() (default on Unix)
19   -c/--clock: use time.clock() (default on Windows)
20   -v/--verbose: print raw timing results; repeat for more digits precision
21   -h/--help: print this usage message and exit
22   statement: statement to be timed (default 'pass')
23
24 A multi-line statement may be given by specifying each line as a
25 separate argument; indented lines are possible by enclosing an
26 argument in quotes and using leading spaces.  Multiple -s options are
27 treated similarly.
28
29 If -n is not given, a suitable number of loops is calculated by trying
30 successive powers of 10 until the total time is at least 0.2 seconds.
31
32 The difference in default timer function is because on Windows,
33 clock() has microsecond granularity but time()'s granularity is 1/60th
34 of a second; on Unix, clock() has 1/100th of a second granularity and
35 time() is much more precise.  On either platform, the default timer
36 functions measure wall clock time, not the CPU time.  This means that
37 other processes running on the same computer may interfere with the
38 timing.  The best thing to do when accurate timing is necessary is to
39 repeat the timing a few times and use the best time.  The -r option is
40 good for this; the default of 3 repetitions is probably enough in most
41 cases.  On Unix, you can use clock() to measure CPU time.
42
43 Note: there is a certain baseline overhead associated with executing a
44 pass statement.  The code here doesn't try to hide it, but you should
45 be aware of it.  The baseline overhead can be measured by invoking the
46 program without arguments.
47
48 The baseline overhead differs between Python versions!  Also, to
49 fairly compare older Python versions to Python 2.3, you may want to
50 use python -O for the older versions to avoid timing SET_LINENO
51 instructions.
52 """
53
54 try:
55     import gc
56 except ImportError:
57     class _fake_gc:
58         def isenabled(self):
59             return None
60         def enable(self):
61             pass
62         def disable(self):
63             pass
64     gc = _fake_gc()
65 import sys
66 import time
67 try:
68     import itertools
69 except ImportError:
70     # Must be an older Python version (see timeit() below)
71     itertools = None
72
73 import string
74
75 __all__ = ["Timer"]
76
77 dummy_src_name = "<timeit-src>"
78 default_number = 1000000
79 default_repeat = 3
80
81 if sys.platform == "win32":
82     # On Windows, the best timer is time.clock()
83     default_timer = time.clock
84 else:
85     # On most other platforms the best timer is time.time()
86     default_timer = time.time
87
88 # Don't change the indentation of the template; the reindent() calls
89 # in Timer.__init__() depend on setup being indented 4 spaces and stmt
90 # being indented 8 spaces.
91 template = """
92 def inner(_it, _timer):
93     %(setup)s
94     _t0 = _timer()
95     for _i in _it:
96         %(stmt)s
97     _t1 = _timer()
98     return _t1 - _t0
99 """
100
101 def reindent(src, indent):
102     """Helper to reindent a multi-line statement."""
103     return string.replace(src, "\n", "\n" + " "*indent)
104
105 class Timer:
106     """Class for timing execution speed of small code snippets.
107
108     The constructor takes a statement to be timed, an additional
109     statement used for setup, and a timer function.  Both statements
110     default to 'pass'; the timer function is platform-dependent (see
111     module doc string).
112
113     To measure the execution time of the first statement, use the
114     timeit() method.  The repeat() method is a convenience to call
115     timeit() multiple times and return a list of results.
116
117     The statements may contain newlines, as long as they don't contain
118     multi-line string literals.
119     """
120
121     def __init__(self, stmt="pass", setup="pass", timer=default_timer):
122         """Constructor.  See class doc string."""
123         self.timer = timer
124         stmt = reindent(stmt, 8)
125         setup = reindent(setup, 4)
126         src = template % {'stmt': stmt, 'setup': setup}
127         self.src = src # Save for traceback display
128         code = compile(src, dummy_src_name, "exec")
129         ns = {}
130         exec code in globals(), ns
131         self.inner = ns["inner"]
132
133     def print_exc(self, file=None):
134         """Helper to print a traceback from the timed code.
135
136         Typical use:
137
138             t = Timer(...)       # outside the try/except
139             try:
140                 t.timeit(...)    # or t.repeat(...)
141             except:
142                 t.print_exc()
143
144         The advantage over the standard traceback is that source lines
145         in the compiled template will be displayed.
146
147         The optional file argument directs where the traceback is
148         sent; it defaults to sys.stderr.
149         """
150         import linecache, traceback
151         linecache.cache[dummy_src_name] = (len(self.src),
152                                            None,
153                                            self.src.split("\n"),
154                                            dummy_src_name)
155         traceback.print_exc(file=file)
156
157     def timeit(self, number=default_number):
158         """Time 'number' executions of the main statement.
159
160         To be precise, this executes the setup statement once, and
161         then returns the time it takes to execute the main statement
162         a number of times, as a float measured in seconds.  The
163         argument is the number of times through the loop, defaulting
164         to one million.  The main statement, the setup statement and
165         the timer function to be used are passed to the constructor.
166         """
167         if itertools:
168             it = itertools.repeat(None, number)
169         else:
170             it = [None] * number
171         gcold = gc.isenabled()
172         gc.disable()
173         timing = self.inner(it, self.timer)
174         if gcold:
175             gc.enable()
176         return timing
177
178     def repeat(self, repeat=default_repeat, number=default_number):
179         """Call timeit() a few times.
180
181         This is a convenience function that calls the timeit()
182         repeatedly, returning a list of results.  The first argument
183         specifies how many times to call timeit(), defaulting to 3;
184         the second argument specifies the timer argument, defaulting
185         to one million.
186
187         Note: it's tempting to calculate mean and standard deviation
188         from the result vector and report these.  However, this is not
189         very useful.  In a typical case, the lowest value gives a
190         lower bound for how fast your machine can run the given code
191         snippet; higher values in the result vector are typically not
192         caused by variability in Python's speed, but by other
193         processes interfering with your timing accuracy.  So the min()
194         of the result is probably the only number you should be
195         interested in.  After that, you should look at the entire
196         vector and apply common sense rather than statistics.
197         """
198         r = []
199         for i in range(repeat):
200             t = self.timeit(number)
201             r.append(t)
202         return r
203
204 def main(args=None):
205     """Main program, used when run as a script.
206
207     The optional argument specifies the command line to be parsed,
208     defaulting to sys.argv[1:].
209
210     The return value is an exit code to be passed to sys.exit(); it
211     may be None to indicate success.
212
213     When an exception happens during timing, a traceback is printed to
214     stderr and the return value is 1.  Exceptions at other times
215     (including the template compilation) are not caught.
216     """
217     if args is None:
218         args = sys.argv[1:]
219     import getopt
220     try:
221         opts, args = getopt.getopt(args, "n:s:r:tcvh",
222                                    ["number=", "setup=", "repeat=",
223                                     "time", "clock", "verbose", "help"])
224     except getopt.error, err:
225         print err
226         print "use -h/--help for command line help"
227         return 2
228     timer = default_timer
229     stmt = string.join(args, "\n") or "pass"
230     number = 0 # auto-determine
231     setup = []
232     repeat = default_repeat
233     verbose = 0
234     precision = 3
235     for o, a in opts:
236         if o in ("-n", "--number"):
237             number = int(a)
238         if o in ("-s", "--setup"):
239             setup.append(a)
240         if o in ("-r", "--repeat"):
241             repeat = int(a)
242             if repeat <= 0:
243                 repeat = 1
244         if o in ("-t", "--time"):
245             timer = time.time
246         if o in ("-c", "--clock"):
247             timer = time.clock
248         if o in ("-v", "--verbose"):
249             if verbose:
250                 precision = precision + 1
251             verbose = precision + 1
252         if o in ("-h", "--help"):
253             print __doc__,
254             return 0
255     setup = string.join(setup, "\n") or "pass"
256     # Include the current directory, so that local imports work (sys.path
257     # contains the directory of this script, rather than the current
258     # directory)
259     import os
260     sys.path.insert(0, os.curdir)
261     t = Timer(stmt, setup, timer)
262     if number == 0:
263         # determine number so that 0.2 <= total time < 2.0
264         for i in range(1, 10):
265             number = 10**i
266             try:
267                 x = t.timeit(number)
268             except:
269                 t.print_exc()
270                 return 1
271             if verbose:
272                 print "%d loops -> %.*g secs" % (number, precision, x)
273             if x >= 0.2:
274                 break
275     try:
276         r = t.repeat(repeat, number)
277     except:
278         t.print_exc()
279         return 1
280     best = min(r)
281     if verbose:
282         print "raw times:", string.join(map(lambda x, p=precision: "%.*g" % (p, x), r))
283     print "%d loops," % number,
284     usec = best * 1e6 / number
285     if usec < 1000:
286         print "best of %d: %.*g usec per loop" % (repeat, precision, usec)
287     else:
288         msec = usec / 1000
289         if msec < 1000:
290             print "best of %d: %.*g msec per loop" % (repeat, precision, msec)
291         else:
292             sec = msec / 1000
293             print "best of %d: %.*g sec per loop" % (repeat, precision, sec)
294     return None
295
296 if __name__ == "__main__":
297     sys.exit(main())