blob: 6f10bd1410d30d7fd301e07eab7025642a61adf7 [file] [log] [blame]
Marc-André Lemburgc311f642006-04-19 15:27:33 +00001#!/usr/local/bin/python -O
2
3""" A Python Benchmark Suite
4
5"""
6#
7# Note: Please keep this module compatible to Python 1.5.2.
8#
9# Tests may include features in later Python versions, but these
10# should then be embedded in try-except clauses in the configuration
11# module Setup.py.
12#
13
14# pybench Copyright
15__copyright__ = """\
16Copyright (c), 1997-2006, Marc-Andre Lemburg (mal@lemburg.com)
17Copyright (c), 2000-2006, eGenix.com Software GmbH (info@egenix.com)
18
19 All Rights Reserved.
20
21Permission to use, copy, modify, and distribute this software and its
22documentation for any purpose and without fee or royalty is hereby
23granted, provided that the above copyright notice appear in all copies
24and that both that copyright notice and this permission notice appear
25in supporting documentation or portions thereof, including
26modifications, that you make.
27
28THE AUTHOR MARC-ANDRE LEMBURG DISCLAIMS ALL WARRANTIES WITH REGARD TO
29THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
30FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
31INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
32FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
33NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
34WITH THE USE OR PERFORMANCE OF THIS SOFTWARE !
35"""
36
37# Version number
38__version__ = '1.3'
39
40#
41# NOTE: Use xrange for all test loops unless you want to face
42# a 20MB process !
43#
44# All tests should have rounds set to values so that a run()
45# takes between 20-50 seconds. This is to get fairly good
46# clock() values. You can use option -w to speedup the tests
47# by a fixed integer factor (the "warp factor").
48#
49
50import sys,time,operator
51from CommandLine import *
52
53try:
54 import cPickle
55 pickle = cPickle
56except ImportError:
57 import pickle
58
59### Test baseclass
60
61class Test:
62
63 """ All test must have this class as baseclass. It provides
64 the necessary interface to the benchmark machinery.
65
66 The tests must set .rounds to a value high enough to let the
67 test run between 20-50 seconds. This is needed because
68 clock()-timing only gives rather inaccurate values (on Linux,
69 for example, it is accurate to a few hundreths of a
70 second). If you don't want to wait that long, use a warp
71 factor larger than 1.
72
73 It is also important to set the .operations variable to a
74 value representing the number of "virtual operations" done per
75 call of .run().
76
77 If you change a test in some way, don't forget to increase
78 it's version number.
79
80 """
81
82 ### Instance variables that each test should override
83
84 # Version number of the test as float (x.yy); this is important
85 # for comparisons of benchmark runs - tests with unequal version
86 # number will not get compared.
87 version = 1.0
88
89 # The number of abstract operations done in each round of the
90 # test. An operation is the basic unit of what you want to
91 # measure. The benchmark will output the amount of run-time per
92 # operation. Note that in order to raise the measured timings
93 # significantly above noise level, it is often required to repeat
94 # sets of operations more than once per test round. The measured
95 # overhead per test round should be less than 1 second.
96 operations = 1
97
98 # Number of rounds to execute per test run. This should be
99 # adjusted to a figure that results in a test run-time of between
100 # 20-50 seconds.
101 rounds = 100000
102
103 ### Internal variables
104
105 # Mark this class as implementing a test
106 is_a_test = 1
107
108 # Misc. internal variables
109 last_timing = (0,0,0) # last timing (real,run,calibration)
110 warp = 1 # warp factor this test uses
111 cruns = 20 # number of calibration runs
112 overhead = None # list of calibration timings
113
114 def __init__(self,warp=1):
115
116 if warp > 1:
117 self.rounds = self.rounds / warp
118 self.warp = warp
119 self.times = []
120 self.overhead = []
121 # We want these to be in the instance dict, so that pickle
122 # saves them
123 self.version = self.version
124 self.operations = self.operations
125 self.rounds = self.rounds
126
127 def run(self):
128
129 """ Run the test in two phases: first calibrate, then
130 do the actual test. Be careful to keep the calibration
131 timing low w/r to the test timing.
132
133 """
134 test = self.test
135 calibrate = self.calibrate
136 clock = time.clock
137 cruns = self.cruns
138 # first calibrate
139 offset = 0.0
140 for i in range(cruns):
141 t = clock()
142 calibrate()
143 t = clock() - t
144 offset = offset + t
145 offset = offset / cruns
146 # now the real thing
147 t = clock()
148 test()
149 t = clock() - t
150 self.last_timing = (t-offset,t,offset)
151 self.times.append(t-offset)
152
153 def calibrate(self):
154
155 """ Calibrate the test.
156
157 This method should execute everything that is needed to
158 setup and run the test - except for the actual operations
159 that you intend to measure. pybench uses this method to
160 measure the test implementation overhead.
161
162 """
163 return
164
165 def test(self):
166
167 """ Run the test.
168
169 The test needs to run self.rounds executing
170 self.operations number of operations each.
171
172 """
173 # do some tests
174 return
175
176 def stat(self):
177
178 """ Returns two value: average time per run and average per
179 operation.
180
181 """
182 runs = len(self.times)
183 if runs == 0:
184 return 0,0
185 totaltime = reduce(operator.add,self.times,0.0)
186 avg = totaltime / float(runs)
187 op_avg = totaltime / float(runs * self.rounds * self.operations)
188 if self.overhead:
189 totaloverhead = reduce(operator.add,self.overhead,0.0)
190 ov_avg = totaloverhead / float(runs)
191 else:
192 # use self.last_timing - not too accurate
193 ov_avg = self.last_timing[2]
194 return avg,op_avg,ov_avg
195
196### Load Setup
197
198# This has to be done after the definition of the Test class, since
199# the Setup module will import subclasses using this class.
200
201import Setup
202
203### Benchmark base class
204
205class Benchmark:
206
207 name = '?' # Name of the benchmark
208 rounds = 1 # Number of rounds to run
209 warp = 1 # Warp factor
210 roundtime = 0 # Average round time
211 version = None # Benchmark version number (see __init__)
212 # as float x.yy
213 starttime = None # Benchmark start time
214
215 def __init__(self):
216
217 self.tests = {}
218 self.version = 0.31
219
220 def load_tests(self,setupmod,warp=1):
221
222 self.warp = warp
223 tests = self.tests
224 print 'Searching for tests...'
225 setupmod.__dict__.values()
226 for c in setupmod.__dict__.values():
227 if hasattr(c,'is_a_test') and c.__name__ != 'Test':
228 tests[c.__name__] = c(warp)
229 l = tests.keys()
230 l.sort()
231 for t in l:
232 print ' ',t
233 print
234
235 def run(self):
236
237 tests = self.tests.items()
238 tests.sort()
239 clock = time.clock
240 print 'Running %i round(s) of the suite: ' % self.rounds
241 print
242 self.starttime = time.time()
243 roundtime = clock()
244 for i in range(self.rounds):
245 print ' Round %-25i real abs overhead' % (i+1)
246 for j in range(len(tests)):
247 name,t = tests[j]
248 print '%30s:' % name,
249 t.run()
250 print ' %.3fr %.3fa %.3fo' % t.last_timing
251 print ' ----------------------'
252 print ' Average round time: %.3f seconds' % \
253 ((clock() - roundtime)/(i+1))
254 print
255 self.roundtime = (clock() - roundtime) / self.rounds
256 print
257
258 def print_stat(self, compare_to=None, hidenoise=0):
259
260 if not compare_to:
261 print '%-30s per run per oper. overhead' % 'Tests:'
262 print '-'*72
263 tests = self.tests.items()
264 tests.sort()
265 for name,t in tests:
266 avg,op_avg,ov_avg = t.stat()
267 print '%30s: %10.2f ms %7.2f us %7.2f ms' % \
268 (name,avg*1000.0,op_avg*1000000.0,ov_avg*1000.0)
269 print '-'*72
270 print '%30s: %10.2f ms' % \
271 ('Average round time',self.roundtime * 1000.0)
272
273 else:
274 print '%-30s per run per oper. diff *)' % \
275 'Tests:'
276 print '-'*72
277 tests = self.tests.items()
278 tests.sort()
279 compatible = 1
280 for name,t in tests:
281 avg,op_avg,ov_avg = t.stat()
282 try:
283 other = compare_to.tests[name]
284 except KeyError:
285 other = None
286 if other and other.version == t.version and \
287 other.operations == t.operations:
288 avg1,op_avg1,ov_avg1 = other.stat()
289 qop_avg = (op_avg/op_avg1-1.0)*100.0
290 if hidenoise and abs(qop_avg) < 10:
291 qop_avg = ''
292 else:
293 qop_avg = '%+7.2f%%' % qop_avg
294 else:
295 qavg,qop_avg = 'n/a', 'n/a'
296 compatible = 0
297 print '%30s: %10.2f ms %7.2f us %8s' % \
298 (name,avg*1000.0,op_avg*1000000.0,qop_avg)
299 print '-'*72
300 if compatible and compare_to.roundtime > 0 and \
301 compare_to.version == self.version:
302 print '%30s: %10.2f ms %+7.2f%%' % \
303 ('Average round time',self.roundtime * 1000.0,
304 ((self.roundtime*self.warp)/
305 (compare_to.roundtime*compare_to.warp)-1.0)*100.0)
306 else:
307 print '%30s: %10.2f ms n/a' % \
308 ('Average round time',self.roundtime * 1000.0)
309 print
310 print '*) measured against: %s (rounds=%i, warp=%i)' % \
311 (compare_to.name,compare_to.rounds,compare_to.warp)
312 print
313
314def print_machine():
315
316 import platform
317 print 'Machine Details:'
318 print ' Platform ID: %s' % platform.platform()
319 print ' Executable: %s' % sys.executable
320 # There's a bug in Python 2.2b1+...
321 if sys.version[:6] == '2.2b1+':
322 return
323 print ' Python: %s' % platform.python_version()
324 print ' Compiler: %s' % platform.python_compiler()
325 buildno, builddate = platform.python_build()
326 print ' Build: %s (#%s)' % (builddate, buildno)
327
328class PyBenchCmdline(Application):
329
330 header = ("PYBENCH - a benchmark test suite for Python "
331 "interpreters/compilers.")
332
333 version = __version__
334
335 options = [ArgumentOption('-n','number of rounds',Setup.Number_of_rounds),
336 ArgumentOption('-f','save benchmark to file arg',''),
337 ArgumentOption('-c','compare benchmark with the one in file arg',''),
338 ArgumentOption('-s','show benchmark in file arg, then exit',''),
339 SwitchOption('-S','show statistics of benchmarks',0),
340 ArgumentOption('-w','set warp factor to arg',Setup.Warp_factor),
341 SwitchOption('-d','hide noise in compares', 0),
342 SwitchOption('--no-gc','disable garbage collection', 0),
343 ]
344
345 about = """\
346The normal operation is to run the suite and display the
347results. Use -f to save them for later reuse or comparisms.
348
349Examples:
350
351python1.5 pybench.py -w 100 -f p15
352python1.4 pybench.py -w 100 -f p14
353python pybench.py -s p15 -c p14
354"""
355 copyright = __copyright__
356
357 def handle_S(self, value):
358
359 """ Display one line stats for each benchmark file given on the
360 command line.
361
362 """
363 for benchmark in self.files:
364 try:
365 f = open(benchmark, 'rb')
366 bench = pickle.load(f)
367 f.close()
368 except IOError:
369 print '* Error opening/reading file %s' % repr(benchmark)
370 else:
371 print '%s,%-.2f,ms' % (benchmark, bench.roundtime*1000.0)
372 return 0
373
374 def main(self):
375
376 rounds = self.values['-n']
377 reportfile = self.values['-f']
378 show_bench = self.values['-s']
379 compare_to = self.values['-c']
380 hidenoise = self.values['-d']
381 warp = self.values['-w']
382 nogc = self.values['--no-gc']
383
384 # Switch off GC
385 if nogc:
386 try:
387 import gc
388 except ImportError:
389 nogc = 0
390 else:
391 if self.values['--no-gc']:
392 gc.disable()
393
394 print 'PYBENCH',__version__
395 print
396
397 if not compare_to:
398 print_machine()
399 print
400
401 if compare_to:
402 try:
403 f = open(compare_to,'rb')
404 bench = pickle.load(f)
405 bench.name = compare_to
406 f.close()
407 compare_to = bench
408 except IOError:
409 print '* Error opening/reading file',compare_to
410 compare_to = None
411
412 if show_bench:
413 try:
414 f = open(show_bench,'rb')
415 bench = pickle.load(f)
416 bench.name = show_bench
417 f.close()
418 print 'Benchmark: %s (rounds=%i, warp=%i)' % \
419 (bench.name,bench.rounds,bench.warp)
420 print
421 bench.print_stat(compare_to, hidenoise)
422 except IOError:
423 print '* Error opening/reading file',show_bench
424 print
425 return
426
427 if reportfile:
428 if nogc:
429 print 'Benchmark: %s (rounds=%i, warp=%i, no GC)' % \
430 (reportfile,rounds,warp)
431 else:
432 print 'Benchmark: %s (rounds=%i, warp=%i)' % \
433 (reportfile,rounds,warp)
434 print
435
436 # Create benchmark object
437 bench = Benchmark()
438 bench.rounds = rounds
439 bench.load_tests(Setup,warp)
440 try:
441 bench.run()
442 except KeyboardInterrupt:
443 print
444 print '*** KeyboardInterrupt -- Aborting'
445 print
446 return
447 bench.print_stat(compare_to)
448 # ring bell
449 sys.stderr.write('\007')
450
451 if reportfile:
452 try:
453 f = open(reportfile,'wb')
454 bench.name = reportfile
455 pickle.dump(bench,f)
456 f.close()
457 except IOError:
458 print '* Error opening/writing reportfile'
459
460if __name__ == '__main__':
461 PyBenchCmdline()