Skip to content

Commit f899d9c

Browse files
committed
Unify benchmark and testing framework
Makes it much easier and more readable to add new benchmarking testcases. Now just inherit from BenchmarkTestcase. Also share code that was essentially repeated.
1 parent bfc0140 commit f899d9c

File tree

11 files changed

+440
-305
lines changed

11 files changed

+440
-305
lines changed

benchmarks/__main__.py

Lines changed: 7 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -5,171 +5,15 @@
55
Benchmark against pyvsc library for equivalent testcases.
66
'''
77

8-
import unittest
9-
import timeit
10-
import sys
118

12-
from argparse import ArgumentParser
13-
from random import Random
9+
from tests.main import main
1410

15-
from benchmarks.pyvsc.basic import vsc_basic, cr_basic
16-
from benchmarks.pyvsc.in_keyword import vsc_in, cr_in, cr_in_order
17-
from benchmarks.pyvsc.ldinstr import vsc_ldinstr
18-
from benchmarks.pyvsc.randlist import vscRandListSumZero, \
19-
crRandListSumZero, vscRandListUnique, crRandListUnique, \
20-
crRandListSumZeroFaster, crRandListUniqueFaster
21-
from examples.ldinstr import ldInstr
22-
23-
24-
TEST_LENGTH_MULTIPLIER = 1
25-
26-
27-
class BenchmarkTests(unittest.TestCase):
28-
29-
def run_one(self, name, randobj, iterations):
30-
'''
31-
Benchmark one object that implements the .randomize() function
32-
for N iterations.
33-
'''
34-
start_time = timeit.default_timer()
35-
for _ in range(iterations):
36-
randobj.randomize()
37-
end_time = timeit.default_timer()
38-
total_time = end_time - start_time
39-
hz = iterations / total_time
40-
print(f'{self._testMethodName}: {name} took {total_time:.4g}s for {iterations} iterations ({hz:.1f}Hz)')
41-
return total_time, hz
42-
43-
def run_benchmark(self, randobjs, iterations, check):
44-
'''
45-
Reusable function to run a fair benchmark between
46-
two or more randomizable objects.
47-
48-
randobjs: dictionary where key is name, value is an object that implements .randomize()
49-
iterations: how many times to call .randomize()
50-
check: function taking a dictionary of results to check.
51-
'''
52-
iterations *= TEST_LENGTH_MULTIPLIER
53-
results = {}
54-
winner = None
55-
best_hz = 0
56-
for name, randobj in randobjs.items():
57-
total_time, hz = self.run_one(name, randobj, iterations)
58-
if hz > best_hz:
59-
winner = name
60-
best_hz = hz
61-
# Store both total time and Hz in case the test wants to specify
62-
# checks on how long should be taken in wall clock time.
63-
results[name] = total_time, hz
64-
print(f'{self._testMethodName}: The winner is {winner} with {best_hz:.1f}Hz!')
65-
# Print summary of hz delta
66-
for name, (_total_time, hz) in results.items():
67-
if name == winner:
68-
continue
69-
speedup = best_hz / hz
70-
print(f'{self._testMethodName}: {winner} was {speedup:.2f}x faster than {name}')
71-
check(results)
72-
73-
def test_basic(self):
74-
'''
75-
Test basic randomizable object.
76-
'''
77-
randobjs = {'vsc': vsc_basic(), 'cr': cr_basic(Random(0))}
78-
def check(results):
79-
self.assertGreater(results['cr'][1], results['vsc'][1])
80-
# This testcase is typically 40-50x faster, which may vary depending
81-
# on machine. Ensure it doesn't fall below 30x.
82-
speedup = results['cr'][1] / results['vsc'][1]
83-
self.assertGreater(speedup, 30, "Performance has degraded!")
84-
self.run_benchmark(randobjs, 100, check)
85-
86-
def test_in(self):
87-
'''
88-
Test object using 'in' keyword.
89-
'''
90-
randobjs = {
91-
'vsc': vsc_in(),
92-
'cr': cr_in(Random(0)),
93-
'cr_order': cr_in_order(Random(0)),
94-
}
95-
def check(results):
96-
self.assertGreater(results['cr'][1], results['vsc'][1])
97-
# This testcase is typically 13-15x faster, which may vary depending
98-
# on machine. Ensure it doesn't fall below 10x.
99-
speedup = results['cr'][1] / results['vsc'][1]
100-
self.assertGreater(speedup, 10, "Performance has degraded!")
101-
self.run_benchmark(randobjs, 100, check)
102-
103-
def test_ldinstr(self):
104-
'''
105-
Test LD instruction example.
106-
'''
107-
randobjs = {
108-
'vsc': vsc_ldinstr(),
109-
'cr': ldInstr(Random(0)),
110-
}
111-
def check(results):
112-
self.assertGreater(results['cr'][1], results['vsc'][1])
113-
# This testcase is typically 13-15x faster, which may vary depending
114-
# on machine. Ensure it doesn't fall below 10x.
115-
speedup = results['cr'][1] / results['vsc'][1]
116-
self.assertGreater(speedup, 10, "Performance has degraded!")
117-
self.run_benchmark(randobjs, 100, check)
118-
119-
def test_randlist_sumzero(self):
120-
'''
121-
Test random list example where the list must sum to zero.
122-
'''
123-
randobjs = {
124-
'vsc': vscRandListSumZero(),
125-
'cr': crRandListSumZero(Random(0)),
126-
'cr_faster': crRandListSumZeroFaster(Random(0)),
127-
}
128-
def check(results):
129-
self.assertGreater(results['cr'][1], results['vsc'][1])
130-
self.assertGreater(results['cr_faster'][1], results['vsc'][1])
131-
# This testcase is typically 20x faster, which may vary depending
132-
# on machine. Ensure it doesn't fall below 15x.
133-
speedup = results['cr'][1] / results['vsc'][1]
134-
self.assertGreater(speedup, 15, "Performance has degraded!")
135-
speedup = results['cr_faster'][1] / results['vsc'][1]
136-
self.assertGreater(speedup, 15, "Performance has degraded!")
137-
self.run_benchmark(randobjs, 100, check)
138-
139-
def test_randlist_unique(self):
140-
'''
141-
Test random list example where the list must be unique.
142-
'''
143-
randobjs = {
144-
'vsc': vscRandListUnique(),
145-
'cr': crRandListUnique(Random(0)),
146-
'cr_faster': crRandListUniqueFaster(Random(0)),
147-
}
148-
def check(results):
149-
self.assertGreater(results['cr_faster'][1], results['vsc'][1])
150-
self.assertGreater(results['cr'][1], results['vsc'][1])
151-
# With the naive solver, this testcase is typically 3-4x faster,
152-
# which may vary depending on machine. Ensure it doesn't fall
153-
# below 2x.
154-
speedup = results['cr'][1] / results['vsc'][1]
155-
self.assertGreater(speedup, 2, "Performance has degraded!")
156-
# This testcase is typically 10-13x faster, which may vary depending
157-
# on machine. Ensure it doesn't fall below 10x.
158-
speedup = results['cr_faster'][1] / results['vsc'][1]
159-
self.assertGreater(speedup, 10, "Performance has degraded!")
160-
self.run_benchmark(randobjs, 100, check)
161-
162-
163-
def parse_args():
164-
parser = ArgumentParser(description='Run unit tests for constrainedrandom library')
165-
parser.add_argument('--length-mul', type=int, default=1, help='Multiplier for test length, when desiring greater certainty on performance.')
166-
args, extra = parser.parse_known_args()
167-
return args, extra
11+
from benchmarks.pyvsc.basic import VSCBasic
12+
from benchmarks.pyvsc.in_keyword import VSCIn
13+
from benchmarks.pyvsc.ldinstr import VSCInstr
14+
from benchmarks.pyvsc.randlist import VSCRandListSumZero
15+
from benchmarks.pyvsc.randlist import VSCRandListUnique
16816

16917

17018
if __name__ == "__main__":
171-
args, extra = parse_args()
172-
TEST_LENGTH_MULTIPLIER = args.length_mul
173-
# Reconstruct argv
174-
argv = [sys.argv[0]] + extra
175-
unittest.main(argv=argv)
19+
main()

benchmarks/benchmark_utils.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import random
2+
from typing import Any, Dict
3+
4+
from tests.perf_utils import PERF_DICT
5+
from tests.testutils import TestBase
6+
7+
8+
class BenchmarkTestCase(TestBase):
9+
'''
10+
Class to be overridden to provide benchmark
11+
test cases.
12+
'''
13+
14+
def test_benchmark(self):
15+
'''
16+
Reusable function to run a fair benchmark between
17+
two or more randomizable objects.
18+
'''
19+
results = {}
20+
winner = None
21+
best_hz = 0
22+
randobjs = self.get_randobjs()
23+
for name, randobj in randobjs.items():
24+
# Attempt fairness by re-seeding each time
25+
random.seed(0)
26+
full_name = self.__class__.__name__ + f".{name}"
27+
_result, perf_result = self.randomize_and_time(randobj, self.iterations, name=full_name)
28+
if perf_result['hz'] > best_hz:
29+
winner = name
30+
best_hz = perf_result['hz']
31+
# Store both total time and Hz in case the test wants to specify
32+
# checks on how long should be taken in wall clock time.
33+
results[name] = perf_result
34+
print(f'{self._testMethodName}: The winner is {winner} with {best_hz:.2f}Hz!')
35+
# Print summary of hz delta
36+
for name, perf_result in results.items():
37+
if name == winner:
38+
continue
39+
speedup = best_hz / perf_result['hz']
40+
print(f'{self._testMethodName}: {winner} was {speedup:.2f}x faster than {name}')
41+
self.check_perf(results)
42+
43+
def check_perf(self, perf_results: Dict[str, PERF_DICT]):
44+
'''
45+
Implement this to check the expected performance results
46+
for a given testcase.
47+
48+
perf_results: Performance results as captured.
49+
'''
50+
raise NotImplementedError("Benchmark test cases should check performance")
51+
52+
def get_randobjs(self) -> Dict[str, Any]:
53+
'''
54+
Implement this to return the objects to test.
55+
56+
Returns a dictionary with index as the string name
57+
of the random object, value as the object which implements
58+
`.randomize()`.
59+
'''
60+
raise NotImplementedError("Benchmark test cases must implement get_randobjs()")

benchmarks/pyvsc/basic.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# SPDX-License-Identifier: MIT
22
# Copyright (c) 2023 Imagination Technologies Ltd. All Rights Reserved
33

4-
'''
5-
Basic random object from pyvsc documentation.
6-
'''
7-
84
from constrainedrandom import RandObj
95
import vsc
106

7+
from ..benchmark_utils import BenchmarkTestCase
8+
119

1210
@vsc.randobj
1311
class vsc_basic(object):
@@ -25,11 +23,48 @@ def ab_c(self):
2523

2624
class cr_basic(RandObj):
2725

28-
def __init__(self, random):
29-
super().__init__(random)
26+
def __init__(self):
27+
super().__init__()
3028
self.add_rand_var('a', bits=8)
3129
self.add_rand_var('b', bits=8, order=1)
3230
self.add_rand_var('c', bits=8)
3331
self.add_rand_var('d', bits=8)
3432

3533
self.add_constraint(lambda a, b : a < b, ('a', 'b'))
34+
35+
36+
class cr_basic_class(RandObj):
37+
38+
def __init__(self):
39+
super().__init__()
40+
self.add_rand_var('a', bits=8)
41+
self.add_rand_var('b', bits=8, order=1)
42+
self.add_rand_var('c', bits=8)
43+
self.add_rand_var('d', bits=8)
44+
self.add_constraint(self.ab_c, ('a', 'b'))
45+
46+
def ab_c(self, a, b):
47+
return a < b
48+
49+
50+
class VSCBasic(BenchmarkTestCase):
51+
'''
52+
Basic random object from pyvsc documentation.
53+
'''
54+
55+
def get_randobjs(self):
56+
return {
57+
'vsc': vsc_basic(),
58+
'cr': cr_basic(),
59+
'cr_class': cr_basic_class(),
60+
}
61+
62+
def check_perf(self, perf_results):
63+
self.assertGreater(perf_results['cr']['hz'], perf_results['vsc']['hz'])
64+
self.assertGreater(perf_results['cr_class']['hz'], perf_results['vsc']['hz'])
65+
# This testcase is typically 40-50x faster, which may vary depending
66+
# on machine. Ensure it doesn't fall below 30x.
67+
speedup = perf_results['cr']['hz'] / perf_results['vsc']['hz']
68+
self.assertGreater(speedup, 30, "Performance has degraded!")
69+
speedup = perf_results['cr_class']['hz'] / perf_results['vsc']['hz']
70+
self.assertGreater(speedup, 30, "Performance has degraded!")

benchmarks/pyvsc/in_keyword.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# SPDX-License-Identifier: MIT
22
# Copyright (c) 2023 Imagination Technologies Ltd. All Rights Reserved
33

4-
'''
5-
Random object using 'in' keyword from pyvsc documentation.
6-
'''
7-
84
from constrainedrandom import RandObj
95
import vsc
106

7+
from ..benchmark_utils import BenchmarkTestCase
8+
119

1210
@vsc.randobj
1311
class vsc_in(object):
@@ -34,8 +32,8 @@ class cr_in(RandObj):
3432
Basic implementation, does thes same thing as vsc_in. No ordering hints.
3533
'''
3634

37-
def __init__(self, random):
38-
super().__init__(random)
35+
def __init__(self):
36+
super().__init__()
3937

4038
self.add_rand_var('a', domain=[1,2] + list(range(4,8)))
4139
self.add_rand_var('b', bits=8, constraints=(lambda b : b != 0,))
@@ -56,8 +54,8 @@ class cr_in_order(RandObj):
5654
cr_in, but with ordering hints.
5755
'''
5856

59-
def __init__(self, random):
60-
super().__init__(random)
57+
def __init__(self):
58+
super().__init__()
6159

6260
self.add_rand_var('a', domain=[1,2] + list(range(4,8)), order=0)
6361
self.add_rand_var('b', bits=8, constraints=(lambda b : b != 0,), order=2)
@@ -71,3 +69,26 @@ def c_lt_d(c, d):
7169
def b_in_range(b, c, d):
7270
return b in range(c, d)
7371
self.add_constraint(b_in_range, ('b', 'c', 'd'))
72+
73+
74+
class VSCIn(BenchmarkTestCase):
75+
'''
76+
Random object using 'in' keyword from pyvsc documentation.
77+
'''
78+
79+
def get_randobjs(self):
80+
return {
81+
'vsc': vsc_in(),
82+
'cr': cr_in(),
83+
'cr_order': cr_in_order(),
84+
}
85+
86+
def check_perf(self, results):
87+
self.assertGreater(results['cr']['hz'], results['vsc']['hz'])
88+
self.assertGreater(results['cr_order']['hz'], results['vsc']['hz'])
89+
# This testcase is typically 13-15x faster, which may vary depending
90+
# on machine. Ensure it doesn't fall below 10x.
91+
speedup = results['cr']['hz'] / results['vsc']['hz']
92+
self.assertGreater(speedup, 10, "Performance has degraded!")
93+
speedup = results['cr_order']['hz'] / results['vsc']['hz']
94+
self.assertGreater(speedup, 10, "Performance has degraded!")

0 commit comments

Comments
 (0)