1 #!/usr/local/bin/python -O
3 """ A Python Benchmark Suite
7 # Note: Please keep this module compatible to Python 1.5.2.
9 # Tests may include features in later Python versions, but these
10 # should then be embedded in try-except clauses in the configuration
16 Copyright (c), 1997-2006, Marc-Andre Lemburg (mal@lemburg.com)
17 Copyright (c), 2000-2006, eGenix.com Software GmbH (info@egenix.com)
21 Permission to use, copy, modify, and distribute this software and its
22 documentation for any purpose and without fee or royalty is hereby
23 granted, provided that the above copyright notice appear in all copies
24 and that both that copyright notice and this permission notice appear
25 in supporting documentation or portions thereof, including
26 modifications, that you make.
28 THE AUTHOR MARC-ANDRE LEMBURG DISCLAIMS ALL WARRANTIES WITH REGARD TO
29 THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
30 FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
31 INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
32 FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
33 NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
34 WITH THE USE OR PERFORMANCE OF THIS SOFTWARE !
37 import sys, time, operator, string, platform
38 from CommandLine import *
46 # Version number; version history: see README file !
58 # Horizontal line length
61 # Minimum test run-time
62 MIN_TEST_RUNTIME = 1e-3
64 # Number of calibration runs to use for calibrating the tests
67 # Number of calibration loops to run for each calibration run
68 CALIBRATION_LOOPS = 20
70 # Allow skipping calibration ?
71 ALLOW_SKIPPING_CALIBRATION = 1
74 TIMER_TIME_TIME = 'time.time'
75 TIMER_TIME_CLOCK = 'time.clock'
76 TIMER_SYSTIMES_PROCESSTIME = 'systimes.processtime'
78 # Choose platform default timer
79 if sys.platform[:3] == 'win':
80 # On WinXP this has 2.5ms resolution
81 TIMER_PLATFORM_DEFAULT = TIMER_TIME_CLOCK
83 # On Linux this has 1ms resolution
84 TIMER_PLATFORM_DEFAULT = TIMER_TIME_TIME
86 # Print debug information ?
91 def get_timer(timertype):
93 if timertype == TIMER_TIME_TIME:
95 elif timertype == TIMER_TIME_CLOCK:
97 elif timertype == TIMER_SYSTIMES_PROCESSTIME:
99 return systimes.processtime
101 raise TypeError('unknown timer type: %s' % timertype)
103 def get_machine_details():
106 print 'Getting machine details...'
107 buildno, builddate = platform.python_build()
108 python = platform.python_version()
112 # UCS2 build (standard)
117 # UCS4 build (most recent Linux distros)
119 bits, linkage = platform.architecture()
121 'platform': platform.platform(),
122 'processor': platform.processor(),
123 'executable': sys.executable,
124 'implementation': getattr(platform, 'python_implementation',
126 'python': platform.python_version(),
127 'compiler': platform.python_compiler(),
129 'builddate': builddate,
134 def print_machine_details(d, indent=''):
136 l = ['Machine Details:',
137 ' Platform ID: %s' % d.get('platform', 'n/a'),
138 ' Processor: %s' % d.get('processor', 'n/a'),
141 ' Implementation: %s' % d.get('implementation', 'n/a'),
142 ' Executable: %s' % d.get('executable', 'n/a'),
143 ' Version: %s' % d.get('python', 'n/a'),
144 ' Compiler: %s' % d.get('compiler', 'n/a'),
145 ' Bits: %s' % d.get('bits', 'n/a'),
146 ' Build: %s (#%s)' % (d.get('builddate', 'n/a'),
147 d.get('buildno', 'n/a')),
148 ' Unicode: %s' % d.get('unicode', 'n/a'),
150 print indent + string.join(l, '\n' + indent) + '\n'
156 """ All test must have this class as baseclass. It provides
157 the necessary interface to the benchmark machinery.
159 The tests must set .rounds to a value high enough to let the
160 test run between 20-50 seconds. This is needed because
161 clock()-timing only gives rather inaccurate values (on Linux,
162 for example, it is accurate to a few hundreths of a
163 second). If you don't want to wait that long, use a warp
164 factor larger than 1.
166 It is also important to set the .operations variable to a
167 value representing the number of "virtual operations" done per
170 If you change a test in some way, don't forget to increase
175 ### Instance variables that each test should override
177 # Version number of the test as float (x.yy); this is important
178 # for comparisons of benchmark runs - tests with unequal version
179 # number will not get compared.
182 # The number of abstract operations done in each round of the
183 # test. An operation is the basic unit of what you want to
184 # measure. The benchmark will output the amount of run-time per
185 # operation. Note that in order to raise the measured timings
186 # significantly above noise level, it is often required to repeat
187 # sets of operations more than once per test round. The measured
188 # overhead per test round should be less than 1 second.
191 # Number of rounds to execute per test run. This should be
192 # adjusted to a figure that results in a test run-time of between
196 ### Internal variables
198 # Mark this class as implementing a test
201 # Last timing: (real, run, overhead)
202 last_timing = (0.0, 0.0, 0.0)
204 # Warp factor to use for this test
207 # Number of calibration runs to use
208 calibration_runs = CALIBRATION_RUNS
210 # List of calibration timings
211 overhead_times = None
213 # List of test run timings
216 # Timer used for the benchmark
217 timer = TIMER_PLATFORM_DEFAULT
219 def __init__(self, warp=None, calibration_runs=None, timer=None):
223 self.rounds = int(self.rounds / warp)
225 raise ValueError('warp factor set too high')
227 if calibration_runs is not None:
228 if (not ALLOW_SKIPPING_CALIBRATION and
229 calibration_runs < 1):
230 raise ValueError('at least one calibration run is required')
231 self.calibration_runs = calibration_runs
232 if timer is not None:
237 self.overhead_times = []
239 # We want these to be in the instance dict, so that pickle
241 self.version = self.version
242 self.operations = self.operations
243 self.rounds = self.rounds
247 """ Return the timer function to use for the test.
250 return get_timer(self.timer)
252 def compatible(self, other):
254 """ Return 1/0 depending on whether the test is compatible
255 with the other Test instance or not.
258 if self.version != other.version:
260 if self.rounds != other.rounds:
264 def calibrate_test(self):
266 if self.calibration_runs == 0:
267 self.overhead_times = [0.0]
270 calibrate = self.calibrate
271 timer = self.get_timer()
272 calibration_loops = range(CALIBRATION_LOOPS)
274 # Time the calibration loop overhead
276 for i in range(self.calibration_runs):
278 for i in calibration_loops:
281 prep_times.append(t / CALIBRATION_LOOPS)
282 min_prep_time = min(prep_times)
285 print 'Calib. prep time = %.6fms' % (
286 min_prep_time * MILLI_SECONDS)
288 # Time the calibration runs (doing CALIBRATION_LOOPS loops of
289 # .calibrate() method calls each)
290 for i in range(self.calibration_runs):
292 for i in calibration_loops:
295 self.overhead_times.append(t / CALIBRATION_LOOPS
298 # Check the measured times
299 min_overhead = min(self.overhead_times)
300 max_overhead = max(self.overhead_times)
302 print 'Calib. overhead time = %.6fms' % (
303 min_overhead * MILLI_SECONDS)
304 if min_overhead < 0.0:
305 raise ValueError('calibration setup did not work')
306 if max_overhead - min_overhead > 0.1:
308 'overhead calibration timing range too inaccurate: '
309 '%r - %r' % (min_overhead, max_overhead))
313 """ Run the test in two phases: first calibrate, then
314 do the actual test. Be careful to keep the calibration
315 timing low w/r to the test timing.
319 timer = self.get_timer()
322 min_overhead = min(self.overhead_times)
328 if t < MIN_TEST_RUNTIME:
329 raise ValueError('warp factor too high: '
330 'test times are < 10ms')
331 eff_time = t - min_overhead
333 raise ValueError('wrong calibration')
334 self.last_timing = (eff_time, t, min_overhead)
335 self.times.append(eff_time)
339 """ Calibrate the test.
341 This method should execute everything that is needed to
342 setup and run the test - except for the actual operations
343 that you intend to measure. pybench uses this method to
344 measure the test implementation overhead.
353 The test needs to run self.rounds executing
354 self.operations number of operations each.
361 """ Return test run statistics as tuple:
366 average time per operation,
367 minimum overhead time)
370 runs = len(self.times)
372 return 0.0, 0.0, 0.0, 0.0
373 min_time = min(self.times)
374 total_time = reduce(operator.add, self.times, 0.0)
375 avg_time = total_time / float(runs)
376 operation_avg = total_time / float(runs
379 if self.overhead_times:
380 min_overhead = min(self.overhead_times)
382 min_overhead = self.last_timing[2]
383 return min_time, avg_time, total_time, operation_avg, min_overhead
387 # This has to be done after the definition of the Test class, since
388 # the Setup module will import subclasses using this class.
392 ### Benchmark base class
396 # Name of the benchmark
399 # Number of benchmark rounds to run
402 # Warp factor use to run the tests
403 warp = 1 # Warp factor
405 # Average benchmark round time
408 # Benchmark version number as float x.yy
411 # Produce verbose output ?
414 # Dictionary with the machine details
415 machine_details = None
417 # Timer used for the benchmark
418 timer = TIMER_PLATFORM_DEFAULT
420 def __init__(self, name, verbose=None, timer=None, warp=None,
421 calibration_runs=None):
426 self.name = '%04i-%02i-%02i %02i:%02i:%02i' % \
427 (time.localtime(time.time())[:6])
428 if verbose is not None:
429 self.verbose = verbose
430 if timer is not None:
434 if calibration_runs is not None:
435 self.calibration_runs = calibration_runs
440 print 'Getting machine details...'
441 self.machine_details = get_machine_details()
443 # Make .version an instance attribute to have it saved in the
445 self.version = self.version
449 """ Return the timer function to use for the test.
452 return get_timer(self.timer)
454 def compatible(self, other):
456 """ Return 1/0 depending on whether the benchmark is
457 compatible with the other Benchmark instance or not.
460 if self.version != other.version:
462 if (self.machine_details == other.machine_details and
463 self.timer != other.timer):
465 if (self.calibration_runs == 0 and
466 other.calibration_runs != 0):
468 if (self.calibration_runs != 0 and
469 other.calibration_runs == 0):
473 def load_tests(self, setupmod, limitnames=None):
477 print 'Searching for tests ...'
478 print '--------------------------------------'
479 for testclass in setupmod.__dict__.values():
480 if not hasattr(testclass, 'is_a_test'):
482 name = testclass.__name__
485 if (limitnames is not None and
486 limitnames.search(name) is None):
488 self.tests[name] = testclass(
490 calibration_runs=self.calibration_runs,
492 l = self.tests.keys()
497 print '--------------------------------------'
498 print ' %i tests found' % len(l)
503 print 'Calibrating tests. Please wait...',
510 tests = self.tests.items()
512 for i in range(len(tests)):
513 name, test = tests[i]
514 test.calibrate_test()
516 print '%30s: %6.3fms %6.3fms' % \
518 min(test.overhead_times) * MILLI_SECONDS,
519 max(test.overhead_times) * MILLI_SECONDS)
522 print 'Done with the calibration.'
529 tests = self.tests.items()
531 timer = self.get_timer()
532 print 'Running %i round(s) of the suite at warp factor %i:' % \
533 (self.rounds, self.warp)
536 for i in range(self.rounds):
538 print ' Round %-25i effective absolute overhead' % (i+1)
540 for j in range(len(tests)):
541 name, test = tests[j]
543 print '%30s:' % name,
545 (eff_time, abs_time, min_overhead) = test.last_timing
546 total_eff_time = total_eff_time + eff_time
548 print ' %5.0fms %5.0fms %7.3fms' % \
549 (eff_time * MILLI_SECONDS,
550 abs_time * MILLI_SECONDS,
551 min_overhead * MILLI_SECONDS)
552 self.roundtimes.append(total_eff_time)
555 ' ------------------------------')
558 (total_eff_time * MILLI_SECONDS))
561 print '* Round %i done in %.3f seconds.' % (i+1,
567 """ Return benchmark run statistics as tuple:
573 XXX Currently not used, since the benchmark does test
574 statistics across all rounds.
577 runs = len(self.roundtimes)
580 min_time = min(self.roundtimes)
581 total_time = reduce(operator.add, self.roundtimes, 0.0)
582 avg_time = total_time / float(runs)
583 max_time = max(self.roundtimes)
584 return (min_time, avg_time, max_time)
586 def print_header(self, title='Benchmark'):
589 print '%s: %s' % (title, self.name)
592 print ' Rounds: %s' % self.rounds
593 print ' Warp: %s' % self.warp
594 print ' Timer: %s' % self.timer
596 if self.machine_details:
597 print_machine_details(self.machine_details, indent=' ')
600 def print_benchmark(self, hidenoise=0, limitnames=None):
603 ' minimum average operation overhead')
605 tests = self.tests.items()
609 for name, test in tests:
610 if (limitnames is not None and
611 limitnames.search(name) is None):
617 min_overhead) = test.stat()
618 total_min_time = total_min_time + min_time
619 total_avg_time = total_avg_time + avg_time
620 print '%30s: %5.0fms %5.0fms %6.2fus %7.3fms' % \
622 min_time * MILLI_SECONDS,
623 avg_time * MILLI_SECONDS,
624 op_avg * MICRO_SECONDS,
625 min_overhead *MILLI_SECONDS)
629 (total_min_time * MILLI_SECONDS,
630 total_avg_time * MILLI_SECONDS,
634 def print_comparison(self, compare_to, hidenoise=0, limitnames=None):
636 # Check benchmark versions
637 if compare_to.version != self.version:
638 print ('* Benchmark versions differ: '
639 'cannot compare this benchmark to "%s" !' %
642 self.print_benchmark(hidenoise=hidenoise,
643 limitnames=limitnames)
647 compare_to.print_header('Comparing with')
649 ' minimum run-time average run-time')
651 ' this other diff this other diff')
654 # Print test comparisons
655 tests = self.tests.items()
657 total_min_time = other_total_min_time = 0.0
658 total_avg_time = other_total_avg_time = 0.0
659 benchmarks_compatible = self.compatible(compare_to)
661 for name, test in tests:
662 if (limitnames is not None and
663 limitnames.search(name) is None):
669 min_overhead) = test.stat()
670 total_min_time = total_min_time + min_time
671 total_avg_time = total_avg_time + avg_time
673 other = compare_to.tests[name]
677 # Other benchmark doesn't include the given test
678 min_diff, avg_diff = 'n/a', 'n/a'
687 other_min_overhead) = other.stat()
688 other_total_min_time = other_total_min_time + other_min_time
689 other_total_avg_time = other_total_avg_time + other_avg_time
690 if (benchmarks_compatible and
691 test.compatible(other)):
692 # Both benchmark and tests are comparable
693 min_diff = ((min_time * self.warp) /
694 (other_min_time * other.warp) - 1.0)
695 avg_diff = ((avg_time * self.warp) /
696 (other_avg_time * other.warp) - 1.0)
697 if hidenoise and abs(min_diff) < 10.0:
700 min_diff = '%+5.1f%%' % (min_diff * PERCENT)
701 if hidenoise and abs(avg_diff) < 10.0:
704 avg_diff = '%+5.1f%%' % (avg_diff * PERCENT)
706 # Benchmark or tests are not comparable
707 min_diff, avg_diff = 'n/a', 'n/a'
709 print '%30s: %5.0fms %5.0fms %7s %5.0fms %5.0fms %7s' % \
711 min_time * MILLI_SECONDS,
712 other_min_time * MILLI_SECONDS * compare_to.warp / self.warp,
714 avg_time * MILLI_SECONDS,
715 other_avg_time * MILLI_SECONDS * compare_to.warp / self.warp,
719 # Summarise test results
720 if not benchmarks_compatible or not tests_compatible:
721 min_diff, avg_diff = 'n/a', 'n/a'
723 if other_total_min_time != 0.0:
724 min_diff = '%+5.1f%%' % (
725 ((total_min_time * self.warp) /
726 (other_total_min_time * compare_to.warp) - 1.0) * PERCENT)
729 if other_total_avg_time != 0.0:
730 avg_diff = '%+5.1f%%' % (
731 ((total_avg_time * self.warp) /
732 (other_total_avg_time * compare_to.warp) - 1.0) * PERCENT)
736 ' %5.0fms %5.0fms %7s %5.0fms %5.0fms %7s' %
737 (total_min_time * MILLI_SECONDS,
738 (other_total_min_time * compare_to.warp/self.warp
741 total_avg_time * MILLI_SECONDS,
742 (other_total_avg_time * compare_to.warp/self.warp
747 print '(this=%s, other=%s)' % (self.name,
751 class PyBenchCmdline(Application):
753 header = ("PYBENCH - a benchmark test suite for Python "
754 "interpreters/compilers.")
756 version = __version__
760 options = [ArgumentOption('-n',
762 Setup.Number_of_rounds),
764 'save benchmark to file arg',
767 'compare benchmark with the one in file arg',
770 'show benchmark in file arg, then exit',
773 'set warp factor to arg',
776 'run only tests with names matching arg',
779 'set the number of calibration runs to arg',
782 'hide noise in comparisons',
785 'verbose output (not recommended)',
787 SwitchOption('--with-gc',
788 'enable garbage collection',
790 SwitchOption('--with-syscheck',
791 'use default sys check interval',
793 ArgumentOption('--timer',
795 TIMER_PLATFORM_DEFAULT),
799 The normal operation is to run the suite and display the
800 results. Use -f to save them for later reuse or comparisons.
810 python2.1 pybench.py -f p21.pybench
811 python2.5 pybench.py -f p25.pybench
812 python pybench.py -s p25.pybench -c p21.pybench
814 copyright = __copyright__
818 rounds = self.values['-n']
819 reportfile = self.values['-f']
820 show_bench = self.values['-s']
821 compare_to = self.values['-c']
822 hidenoise = self.values['-d']
823 warp = int(self.values['-w'])
824 withgc = self.values['--with-gc']
825 limitnames = self.values['-t']
828 print '* limiting test names to one with substring "%s"' % \
830 limitnames = re.compile(limitnames, re.I)
833 verbose = self.verbose
834 withsyscheck = self.values['--with-syscheck']
835 calibration_runs = self.values['-C']
836 timer = self.values['--timer']
839 print 'PYBENCH %s' % __version__
841 print '* using %s %s' % (
842 getattr(platform, 'python_implementation', lambda:'Python')(),
843 string.join(string.split(sys.version), ' '))
845 # Switch off garbage collection
850 print '* Python version doesn\'t support garbage collection'
854 except NotImplementedError:
855 print '* Python version doesn\'t support gc.disable'
857 print '* disabled garbage collection'
859 # "Disable" sys check interval
861 # Too bad the check interval uses an int instead of a long...
864 sys.setcheckinterval(value)
865 except (AttributeError, NotImplementedError):
866 print '* Python version doesn\'t support sys.setcheckinterval'
868 print '* system check interval set to maximum: %s' % value
870 if timer == TIMER_SYSTIMES_PROCESSTIME:
872 print '* using timer: systimes.processtime (%s)' % \
873 systimes.SYSTIMES_IMPLEMENTATION
875 print '* using timer: %s' % timer
881 f = open(compare_to,'rb')
882 bench = pickle.load(f)
883 bench.name = compare_to
886 except IOError, reason:
887 print '* Error opening/reading file %s: %s' % (
894 f = open(show_bench,'rb')
895 bench = pickle.load(f)
896 bench.name = show_bench
900 bench.print_comparison(compare_to,
902 limitnames=limitnames)
904 bench.print_benchmark(hidenoise=hidenoise,
905 limitnames=limitnames)
906 except IOError, reason:
907 print '* Error opening/reading file %s: %s' % (
914 print 'Creating benchmark: %s (rounds=%i, warp=%i)' % \
915 (reportfile, rounds, warp)
918 # Create benchmark object
919 bench = Benchmark(reportfile,
923 calibration_runs=calibration_runs)
924 bench.rounds = rounds
925 bench.load_tests(Setup, limitnames=limitnames)
929 except KeyboardInterrupt:
931 print '*** KeyboardInterrupt -- Aborting'
936 bench.print_comparison(compare_to,
938 limitnames=limitnames)
940 bench.print_benchmark(hidenoise=hidenoise,
941 limitnames=limitnames)
944 sys.stderr.write('\007')
948 f = open(reportfile,'wb')
949 bench.name = reportfile
952 except IOError, reason:
953 print '* Error opening/writing reportfile'
954 except IOError, reason:
955 print '* Error opening/writing reportfile %s: %s' % (
960 if __name__ == '__main__':