plot_strings.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. #!/usr/bin/python3
  2. # Plot GNU C Library string microbenchmark output.
  3. # Copyright (C) 2019-2026 Free Software Foundation, Inc.
  4. # This file is part of the GNU C Library.
  5. #
  6. # The GNU C Library is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU Lesser General Public
  8. # License as published by the Free Software Foundation; either
  9. # version 2.1 of the License, or (at your option) any later version.
  10. #
  11. # The GNU C Library is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. # Lesser General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public
  17. # License along with the GNU C Library; if not, see
  18. # <https://www.gnu.org/licenses/>.
  19. """Plot string microbenchmark results.
  20. Given a benchmark results file in JSON format and a benchmark schema file,
  21. plot the benchmark timings in one of the available representations.
  22. Separate figure is generated and saved to a file for each 'results' array
  23. found in the benchmark results file. Output filenames and plot titles
  24. are derived from the metadata found in the benchmark results file.
  25. """
  26. import argparse
  27. from collections import defaultdict
  28. import json
  29. import matplotlib as mpl
  30. import numpy as np
  31. import os
  32. import sys
  33. try:
  34. import jsonschema as validator
  35. except ImportError:
  36. print("Could not find jsonschema module.")
  37. raise
  38. # Use pre-selected markers for plotting lines to improve readability
  39. markers = [".", "x", "^", "+", "*", "v", "1", ">", "s"]
  40. # Benchmark variants for which the x-axis scale should be logarithmic
  41. log_variants = {"powers of 2"}
  42. def gmean(numbers):
  43. """Compute geometric mean.
  44. Args:
  45. numbers: 2-D list of numbers
  46. Return:
  47. numpy array with geometric means of numbers along each column
  48. """
  49. a = np.array(numbers, dtype=complex)
  50. means = a.prod(0) ** (1.0 / len(a))
  51. return np.real(means)
  52. def relativeDifference(x, x_reference):
  53. """Compute per-element relative difference between each row of
  54. a matrix and an array of reference values.
  55. Args:
  56. x: numpy matrix of shape (n, m)
  57. x_reference: numpy array of size m
  58. Return:
  59. relative difference between rows of x and x_reference (in %)
  60. """
  61. abs_diff = np.subtract(x, x_reference)
  62. return np.divide(np.multiply(abs_diff, 100.0), x_reference)
  63. def plotTime(timings, routine, bench_variant, title, outpath):
  64. """Plot absolute timing values.
  65. Args:
  66. timings: timings to plot
  67. routine: benchmarked string routine name
  68. bench_variant: top-level benchmark variant name
  69. title: figure title (generated so far)
  70. outpath: output file path (generated so far)
  71. Return:
  72. y: y-axis values to plot
  73. title_final: final figure title
  74. outpath_final: file output file path
  75. """
  76. y = timings
  77. plt.figure()
  78. if not args.values:
  79. plt.axes().yaxis.set_major_formatter(plt.NullFormatter())
  80. plt.ylabel("timing")
  81. title_final = "%s %s benchmark timings\n%s" % \
  82. (routine, bench_variant, title)
  83. outpath_final = os.path.join(args.outdir, "%s_%s_%s%s" % \
  84. (routine, args.plot, bench_variant, outpath))
  85. return y, title_final, outpath_final
  86. def plotRelative(timings, all_timings, routine, ifuncs, bench_variant,
  87. title, outpath):
  88. """Plot timing values relative to a chosen ifunc
  89. Args:
  90. timings: timings to plot
  91. all_timings: all collected timings
  92. routine: benchmarked string routine name
  93. ifuncs: names of ifuncs tested
  94. bench_variant: top-level benchmark variant name
  95. title: figure title (generated so far)
  96. outpath: output file path (generated so far)
  97. Return:
  98. y: y-axis values to plot
  99. title_final: final figure title
  100. outpath_final: file output file path
  101. """
  102. # Choose the baseline ifunc
  103. if args.baseline:
  104. baseline = args.baseline.replace("__", "")
  105. else:
  106. baseline = ifuncs[0]
  107. baseline_index = ifuncs.index(baseline)
  108. # Compare timings against the baseline
  109. y = relativeDifference(timings, all_timings[baseline_index])
  110. plt.figure()
  111. plt.axhspan(-args.threshold, args.threshold, color="lightgray", alpha=0.3)
  112. plt.axhline(0, color="k", linestyle="--", linewidth=0.4)
  113. plt.ylabel("relative timing (in %)")
  114. title_final = "Timing comparison against %s\nfor %s benchmark, %s" % \
  115. (baseline, bench_variant, title)
  116. outpath_final = os.path.join(args.outdir, "%s_%s_%s%s" % \
  117. (baseline, args.plot, bench_variant, outpath))
  118. return y, title_final, outpath_final
  119. def plotMax(timings, routine, bench_variant, title, outpath):
  120. """Plot results as percentage of the maximum ifunc performance.
  121. The optimal ifunc is computed on a per-parameter-value basis.
  122. Performance is computed as 1/timing.
  123. Args:
  124. timings: timings to plot
  125. routine: benchmarked string routine name
  126. bench_variant: top-level benchmark variant name
  127. title: figure title (generated so far)
  128. outpath: output file path (generated so far)
  129. Return:
  130. y: y-axis values to plot
  131. title_final: final figure title
  132. outpath_final: file output file path
  133. """
  134. perf = np.reciprocal(timings)
  135. max_perf = np.max(perf, axis=0)
  136. y = np.add(100.0, relativeDifference(perf, max_perf))
  137. plt.figure()
  138. plt.axhline(100.0, color="k", linestyle="--", linewidth=0.4)
  139. plt.ylabel("1/timing relative to max (in %)")
  140. title_final = "Performance comparison against max for %s\n%s " \
  141. "benchmark, %s" % (routine, bench_variant, title)
  142. outpath_final = os.path.join(args.outdir, "%s_%s_%s%s" % \
  143. (routine, args.plot, bench_variant, outpath))
  144. return y, title_final, outpath_final
  145. def plotThroughput(timings, params, routine, bench_variant, title, outpath):
  146. """Plot throughput.
  147. Throughput is computed as the varied parameter value over timing.
  148. Args:
  149. timings: timings to plot
  150. params: varied parameter values
  151. routine: benchmarked string routine name
  152. bench_variant: top-level benchmark variant name
  153. title: figure title (generated so far)
  154. outpath: output file path (generated so far)
  155. Return:
  156. y: y-axis values to plot
  157. title_final: final figure title
  158. outpath_final: file output file path
  159. """
  160. y = np.divide(params, timings)
  161. plt.figure()
  162. if not args.values:
  163. plt.axes().yaxis.set_major_formatter(plt.NullFormatter())
  164. plt.ylabel("%s / timing" % args.key)
  165. title_final = "%s %s benchmark throughput results\n%s" % \
  166. (routine, bench_variant, title)
  167. outpath_final = os.path.join(args.outdir, "%s_%s_%s%s" % \
  168. (routine, args.plot, bench_variant, outpath))
  169. return y, title_final, outpath_final
  170. def finishPlot(x, y, title, outpath, x_scale, plotted_ifuncs):
  171. """Finish generating current Figure.
  172. Args:
  173. x: x-axis values
  174. y: y-axis values
  175. title: figure title
  176. outpath: output file path
  177. x_scale: x-axis scale
  178. plotted_ifuncs: names of ifuncs to plot
  179. """
  180. plt.xlabel(args.key)
  181. plt.xscale(x_scale)
  182. plt.title(title)
  183. plt.grid(color="k", linestyle=args.grid, linewidth=0.5, alpha=0.5)
  184. for i in range(len(plotted_ifuncs)):
  185. plt.plot(x, y[i], marker=markers[i % len(markers)],
  186. label=plotted_ifuncs[i])
  187. plt.legend(loc="best", fontsize="small")
  188. plt.savefig("%s_%s.%s" % (outpath, x_scale, args.extension),
  189. format=args.extension, dpi=args.resolution)
  190. if args.display:
  191. plt.show()
  192. plt.close()
  193. def plotRecursive(json_iter, routine, ifuncs, bench_variant, title, outpath,
  194. x_scale):
  195. """Plot benchmark timings.
  196. Args:
  197. json_iter: reference to json object
  198. routine: benchmarked string routine name
  199. ifuncs: names of ifuncs tested
  200. bench_variant: top-level benchmark variant name
  201. title: figure's title (generated so far)
  202. outpath: output file path (generated so far)
  203. x_scale: x-axis scale
  204. """
  205. # RECURSIVE CASE: 'variants' array found
  206. if "variants" in json_iter:
  207. # Continue recursive search for 'results' array. Record the
  208. # benchmark variant (configuration) in order to customize
  209. # the title, filename and X-axis scale for the generated figure.
  210. for variant in json_iter["variants"]:
  211. new_title = "%s%s, " % (title, variant["name"])
  212. new_outpath = "%s_%s" % (outpath, variant["name"].replace(" ", "_"))
  213. new_x_scale = "log" if variant["name"] in log_variants else x_scale
  214. plotRecursive(variant, routine, ifuncs, bench_variant, new_title,
  215. new_outpath, new_x_scale)
  216. return
  217. # BASE CASE: 'results' array found
  218. domain = []
  219. timings = defaultdict(list)
  220. # Collect timings
  221. for result in json_iter["results"]:
  222. domain.append(result[args.key])
  223. timings[result[args.key]].append(result["timings"])
  224. domain = np.unique(np.array(domain))
  225. averages = []
  226. # Compute geometric mean if there are multiple timings for each
  227. # parameter value.
  228. for parameter in domain:
  229. averages.append(gmean(timings[parameter]))
  230. averages = np.array(averages).transpose()
  231. # Choose ifuncs to plot
  232. if isinstance(args.ifuncs, str):
  233. plotted_ifuncs = ifuncs
  234. else:
  235. plotted_ifuncs = [x.replace("__", "") for x in args.ifuncs]
  236. plotted_indices = [ifuncs.index(x) for x in plotted_ifuncs]
  237. plotted_vals = averages[plotted_indices,:]
  238. # Plotting logic specific to each plot type
  239. if args.plot == "time":
  240. codomain, title, outpath = plotTime(plotted_vals, routine,
  241. bench_variant, title, outpath)
  242. elif args.plot == "rel":
  243. codomain, title, outpath = plotRelative(plotted_vals, averages, routine,
  244. ifuncs, bench_variant, title, outpath)
  245. elif args.plot == "max":
  246. codomain, title, outpath = plotMax(plotted_vals, routine,
  247. bench_variant, title, outpath)
  248. elif args.plot == "thru":
  249. codomain, title, outpath = plotThroughput(plotted_vals, domain, routine,
  250. bench_variant, title, outpath)
  251. # Plotting logic shared between plot types
  252. finishPlot(domain, codomain, title, outpath, x_scale, plotted_ifuncs)
  253. def main(args):
  254. """Program Entry Point.
  255. Args:
  256. args: command line arguments (excluding program name)
  257. """
  258. # Select non-GUI matplotlib backend if interactive display is disabled
  259. if not args.display:
  260. mpl.use("Agg")
  261. global plt
  262. import matplotlib.pyplot as plt
  263. schema = None
  264. with open(args.schema, "r") as f:
  265. schema = json.load(f)
  266. for filename in args.bench:
  267. bench = None
  268. if filename == '-':
  269. bench = json.load(sys.stdin)
  270. else:
  271. with open(filename, "r") as f:
  272. bench = json.load(f)
  273. validator.validate(bench, schema)
  274. for function in bench["functions"]:
  275. bench_variant = bench["functions"][function]["bench-variant"]
  276. ifuncs = bench["functions"][function]["ifuncs"]
  277. ifuncs = [x.replace("__", "") for x in ifuncs]
  278. plotRecursive(bench["functions"][function], function, ifuncs,
  279. bench_variant, "", "", args.logarithmic)
  280. """ main() """
  281. if __name__ == "__main__":
  282. parser = argparse.ArgumentParser(description=
  283. "Plot string microbenchmark results",
  284. formatter_class=argparse.ArgumentDefaultsHelpFormatter)
  285. # Required parameter
  286. parser.add_argument("bench", nargs="+",
  287. help="benchmark results file(s) in json format, " \
  288. "and/or '-' as a benchmark result file from stdin")
  289. # Optional parameters
  290. parser.add_argument("-b", "--baseline", type=str,
  291. help="baseline ifunc for 'rel' plot")
  292. parser.add_argument("-d", "--display", action="store_true",
  293. help="display figures")
  294. parser.add_argument("-e", "--extension", type=str, default="png",
  295. choices=["png", "pdf", "svg"],
  296. help="output file(s) extension")
  297. parser.add_argument("-g", "--grid", action="store_const", default="",
  298. const="-", help="show grid lines")
  299. parser.add_argument("-i", "--ifuncs", nargs="+", default="all",
  300. help="ifuncs to plot")
  301. parser.add_argument("-k", "--key", type=str, default="length",
  302. help="key to access the varied parameter")
  303. parser.add_argument("-l", "--logarithmic", action="store_const",
  304. default="linear", const="log",
  305. help="use logarithmic x-axis scale")
  306. parser.add_argument("-o", "--outdir", type=str, default=os.getcwd(),
  307. help="output directory")
  308. parser.add_argument("-p", "--plot", type=str, default="time",
  309. choices=["time", "rel", "max", "thru"],
  310. help="plot absolute timings, relative timings, " \
  311. "performance relative to max, or throughput")
  312. parser.add_argument("-r", "--resolution", type=int, default=100,
  313. help="dpi resolution for the generated figures")
  314. parser.add_argument("-s", "--schema", type=str,
  315. default=os.path.join(os.path.dirname(
  316. os.path.realpath(__file__)),
  317. "benchout_strings.schema.json"),
  318. help="schema file to validate the results file.")
  319. parser.add_argument("-t", "--threshold", type=int, default=5,
  320. help="threshold to mark in 'rel' graph (in %%)")
  321. parser.add_argument("-v", "--values", action="store_true",
  322. help="show actual values")
  323. args = parser.parse_args()
  324. main(args)