benchmark.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #!/usr/bin/env python3
  2. import ctypes
  3. import os
  4. import datetime
  5. import sys
  6. class cBenchCounters(ctypes.Structure):
  7. '''
  8. This has to match the returned struct in libbag.c
  9. '''
  10. _fields_ = [ ("additions", ctypes.c_int),
  11. ("failed_removals", ctypes.c_int),
  12. ("successful_removals", ctypes.c_int) ]
  13. class cBenchResult(ctypes.Structure):
  14. '''
  15. This has to match the returned struct in libbag.c
  16. '''
  17. _fields_ = [ ("time", ctypes.c_float),
  18. ("add_time", ctypes.c_float),
  19. ("try_remove_any_time", ctypes.c_float),
  20. ("counters", cBenchCounters) ]
  21. class Benchmark:
  22. '''
  23. Class representing a benchmark. It assumes any benchmark sweeps over some
  24. parameter xrange using the fixed set of inputs for every point. It provides
  25. two ways of averaging over the given amount of repetitions:
  26. - represent everything in a boxplot, or
  27. - average over the results.
  28. '''
  29. def __init__(self, bench_function, parameters,
  30. repetitions_per_point, xrange, basedir, name, now):
  31. self.bench_function = bench_function
  32. self.parameters = parameters
  33. self.repetitions_per_point = repetitions_per_point
  34. self.xrange = xrange
  35. self.basedir = basedir
  36. self.name = name
  37. self.result_times = {}
  38. self.result_add_times = {}
  39. self.result_try_remove_any_times = {}
  40. self.result_throughput = {}
  41. self.result_add_throughput = {}
  42. self.result_try_remove_any_throughput = {}
  43. self.now = now
  44. def __str__(self):
  45. return f'Benchmark(name={self.name}, xrange={self.xrange}, repetitions={self.repetitions_per_point})'
  46. def run(self):
  47. '''
  48. Runs the benchmark with the given parameters. Collects
  49. repetitions_per_point data points and writes them back to the data
  50. dictionary to be processed later.
  51. '''
  52. print(f"Starting Benchmark run at {self.now}")
  53. # for thread count (x-axis)
  54. for x in self.xrange:
  55. times = []
  56. add_times = []
  57. try_remove_any_times = []
  58. throughput = []
  59. add_throughput = []
  60. try_remove_any_throughput = []
  61. for r in range(0, self.repetitions_per_point):
  62. #print(f'run {r} out of {self.repetitions_per_point}')
  63. # evaluate any lambda function parameters (functions of thread count x)
  64. evaluated = [x] + [item(x) if callable(item) else item for item in self.parameters]
  65. # unpack self.parameters tuple (a, b, ...) into parameter list a, b, ...
  66. result = self.bench_function(*evaluated)
  67. times.append(result.time * 1000)
  68. throughput.append(result.counters.successful_removals / result.time)
  69. if result.add_time > 0:
  70. add_times.append(result.add_time * 1000)
  71. add_throughput.append(result.counters.additions / result.add_time)
  72. if result.try_remove_any_time > 0:
  73. try_remove_any_times.append(result.try_remove_any_time * 1000)
  74. try_remove_any_throughput.append(result.counters.successful_removals / result.try_remove_any_time)
  75. self.result_times[x] = times
  76. self.result_throughput[x] = throughput
  77. if len(add_times) > 0:
  78. self.result_add_times[x] = add_times
  79. if len(try_remove_any_times) > 0:
  80. self.result_try_remove_any_times[x] = try_remove_any_times
  81. if len(add_throughput) > 0:
  82. self.result_add_throughput[x] = add_throughput
  83. if len(try_remove_any_throughput) > 0:
  84. self.result_try_remove_any_throughput[x] = try_remove_any_throughput
  85. def write_avg_data(self, extended=False):
  86. '''
  87. Writes averages for each point measured into a dataset in the data
  88. folder timestamped when the run was started.
  89. '''
  90. if self.now is None:
  91. raise Exception("Benchmark was not run or timestamp was not set. Run before writing data.")
  92. try:
  93. os.makedirs(f"{self.basedir}/data/{self.now}/avg/time")
  94. os.makedirs(f"{self.basedir}/data/{self.now}/avg/throughput")
  95. except FileExistsError:
  96. pass
  97. with open(f"{self.basedir}/data/{self.now}/avg/time/{self.name}.data", "w")\
  98. as datafile:
  99. datafile.write(f"x datapoint\n")
  100. for x, box in self.result_times.items():
  101. datafile.write(f"{x} {sum(box)/len(box)}\n")
  102. with open(f"{self.basedir}/data/{self.now}/avg/throughput/{self.name}.data", "w")\
  103. as datafile:
  104. datafile.write(f"x datapoint\n")
  105. for x, box in self.result_throughput.items():
  106. datafile.write(f"{x} {sum(box)/len(box)}\n")
  107. if len(self.result_add_times) > 0:
  108. with open(f"{self.basedir}/data/{self.now}/avg/time/{self.name}.add.data", "w")\
  109. as datafile:
  110. datafile.write(f"x datapoint\n")
  111. for x, box in self.result_add_times.items():
  112. datafile.write(f"{x} {sum(box)/len(box)}\n")
  113. with open(f"{self.basedir}/data/{self.now}/avg/throughput/{self.name}.add.data", "w")\
  114. as datafile:
  115. datafile.write(f"x datapoint\n")
  116. for x, box in self.result_add_throughput.items():
  117. datafile.write(f"{x} {sum(box)/len(box)}\n")
  118. if len(self.result_try_remove_any_times) > 0:
  119. with open(f"{self.basedir}/data/{self.now}/avg/time/{self.name}.tra.data", "w")\
  120. as datafile:
  121. datafile.write(f"x datapoint\n")
  122. for x, box in self.result_try_remove_any_times.items():
  123. datafile.write(f"{x} {sum(box)/len(box)}\n")
  124. with open(f"{self.basedir}/data/{self.now}/avg/throughput/{self.name}.tra.data", "w")\
  125. as datafile:
  126. datafile.write(f"x datapoint\n")
  127. for x, box in self.result_try_remove_any_throughput.items():
  128. datafile.write(f"{x} {sum(box)/len(box)}\n")
  129. def benchmark(parameters):
  130. '''
  131. Requires the binary to also be present as a shared library.
  132. '''
  133. basedir = os.path.dirname(os.path.abspath(__file__))
  134. binary = ctypes.CDLL( f"{basedir}/libbag.so" )
  135. # Set the result type for each benchmark function
  136. binary.bench_simple_prod_cons.restype = cBenchResult
  137. binary.bench_cbag_prod_cons.restype = cBenchResult
  138. binary.bench_simple_add_try_remove_any.restype = cBenchResult
  139. binary.bench_cbag_add_try_remove_any.restype = cBenchResult
  140. # try to get parameterized workload, otherwise return default parameter
  141. arg_workloads = parameters.get('workload', '100,1000,10000')
  142. # try to get parameterized threads, otherwise return default parameter
  143. arg_threads = parameters.get('threads', '2,4,8,16,32,64')
  144. # try to get parameterized repetition count, otherwise return default parameter
  145. arg_repetitions = parameters.get('repetitions', '2')
  146. # try to get parameterized file prefix (default=small)
  147. arg_prefix = parameters.get('prefix', 'small')
  148. # try to get parameterized flag for whether we are running simple benchmarks
  149. arg_simple = parameters.get('simple', 'true')
  150. # try to get parameterized flag for whether we are running cbag benchmarks
  151. arg_cbag = parameters.get('cbag', 'true')
  152. # try to get parameterized flag for whether we are running prod cons (half half)
  153. arg_prod_cons = parameters.get('prod_cons', 'true')
  154. # try to get parameterized flag for whether we are running 1 prod rest cons
  155. arg_1_prod = parameters.get('1_prod', 'true')
  156. # try to get parameterized flag for whether we are running 1 cons rest prod
  157. arg_1_cons = parameters.get('1_cons', 'true')
  158. # try to get parameterized flag for whether we are running add/try_remove_any
  159. arg_add_tra = parameters.get('add_try_remove_any', 'true')
  160. # The number of elements to benchmark
  161. workloads = [int(n) for n in arg_workloads.split(',')]
  162. # The number of threads. This is the x-axis in the benchmark, i.e., the
  163. # parameter that is 'sweeped' over.
  164. num_threads = [int(t) for t in arg_threads.split(',')]
  165. # How often to repeat one benchmark
  166. repetitions = int(arg_repetitions)
  167. # use simple
  168. simple = arg_simple == 'true'
  169. # use cbag
  170. cbag = arg_cbag == 'true'
  171. # use prod_cons
  172. use_prod_cons = arg_prod_cons == 'true'
  173. # use 1_prod
  174. use_1_prod = arg_1_prod == 'true'
  175. # use 1_cons
  176. use_1_cons = arg_1_cons == 'true'
  177. # use add_try_remove_any
  178. use_add_tra = arg_add_tra == 'true'
  179. # The timestamp to mark every benchmark with
  180. #now = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
  181. now = datetime.datetime.now().strftime("%Y-%m-%d")
  182. benchmarks = []
  183. for n in workloads:
  184. # Parameters for the benchmark are passed in a tuple, here (1000,). To pass
  185. # just one parameter, we cannot write (1000) because that would not parse
  186. # as a tuple, instead python understands a trailing comma as a tuple with
  187. # just one entry.
  188. # 30 repetitions per point
  189. if simple:
  190. if use_prod_cons:
  191. # benchmark prod/cons with n/2 threads out of n assigned consumer
  192. benchmarks.append(Benchmark(binary.bench_simple_prod_cons, (n, lambda x: x//2), repetitions, num_threads, basedir, f"{arg_prefix}bench_simple_prod_cons_{n}", now))
  193. if use_1_cons:
  194. # benchmark prod/cons with 1 threads out of n assigned consumer
  195. benchmarks.append(Benchmark(binary.bench_simple_prod_cons, (n, lambda x: 1), repetitions, num_threads, basedir, f"{arg_prefix}bench_simple_1_cons_{n}", now))
  196. if use_1_prod:
  197. # benchmark prod/cons with n - 1 threads out of n assigned consumer
  198. benchmarks.append(Benchmark(binary.bench_simple_prod_cons, (n, lambda x: x - 1), repetitions, num_threads, basedir, f"{arg_prefix}bench_simple_1_prod_{n}", now))
  199. if use_add_tra:
  200. # benchmark add and try_remove_any times
  201. benchmarks.append(Benchmark(binary.bench_simple_add_try_remove_any, (n,), repetitions, num_threads, basedir, f"{arg_prefix}bench_simple_add_tra_{n}", now))
  202. if cbag:
  203. if use_prod_cons:
  204. # benchmark prod/cons with n/2 threads out of n assigned consumer
  205. benchmarks.append(Benchmark(binary.bench_cbag_prod_cons, (n, lambda x: x//2), repetitions, num_threads, basedir, f"{arg_prefix}bench_cbag_prod_cons_{n}", now))
  206. if use_1_cons:
  207. # benchmark prod/cons with 1 threads out of n assigned consumer
  208. benchmarks.append(Benchmark(binary.bench_cbag_prod_cons, (n, lambda x: 1), repetitions, num_threads, basedir, f"{arg_prefix}bench_cbag_1_cons_{n}", now))
  209. if use_1_prod:
  210. # benchmark prod/cons with n - 1 threads out of n assigned consumer
  211. benchmarks.append(Benchmark(binary.bench_cbag_prod_cons, (n, lambda x: x - 1), repetitions, num_threads, basedir, f"{arg_prefix}bench_cbag_1_prod_{n}", now))
  212. if use_add_tra:
  213. # benchmark add and try_remove_any times
  214. benchmarks.append(Benchmark(binary.bench_cbag_add_try_remove_any, (n,), repetitions, num_threads, basedir, f"{arg_prefix}bench_cbag_add_tra_{n}", now))
  215. for benchmark in benchmarks:
  216. benchmark.run()
  217. benchmark.write_avg_data()
  218. print(f'successfully ran benchmark {benchmark}')
  219. for benchmark in benchmarks:
  220. print(benchmark)
  221. if __name__ == "__main__":
  222. # convert every double pair of additional command line arguments into key-value pair in dict
  223. additional_args = sys.argv[1:]
  224. parameters = {key.strip('-'): value for key, value in zip(additional_args[::2], additional_args[1::2])}
  225. benchmark(parameters)