2 # Plot GNU C Library string microbenchmark output.
3 # Copyright (C) 2019-2020 Free Software Foundation, Inc.
4 # This file is part of the GNU C Library.
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.
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.
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.
21 Given a benchmark results file in JSON format and a benchmark schema file,
22 plot the benchmark timings in one of the available representations.
24 Separate figure is generated and saved to a file for each 'results' array
25 found in the benchmark results file. Output filenames and plot titles
26 are derived from the metadata found in the benchmark results file.
29 from collections
import defaultdict
31 import matplotlib
as mpl
36 import jsonschema
as validator
38 print("Could not find jsonschema module.")
41 # Use pre-selected markers for plotting lines to improve readability
42 markers
= [".", "x", "^", "+", "*", "v", "1", ">", "s"]
44 # Benchmark variants for which the x-axis scale should be logarithmic
45 log_variants
= {"powers of 2"}
49 """Compute geometric mean.
52 numbers: 2-D list of numbers
54 numpy array with geometric means of numbers along each column
56 a
= np
.array(numbers
, dtype
=np
.complex)
57 means
= a
.prod(0) ** (1.0 / len(a
))
61 def relativeDifference(x
, x_reference
):
62 """Compute per-element relative difference between each row of
63 a matrix and an array of reference values.
66 x: numpy matrix of shape (n, m)
67 x_reference: numpy array of size m
69 relative difference between rows of x and x_reference (in %)
71 abs_diff
= np
.subtract(x
, x_reference
)
72 return np
.divide(np
.multiply(abs_diff
, 100.0), x_reference
)
75 def plotTime(timings
, routine
, bench_variant
, title
, outpath
):
76 """Plot absolute timing values.
79 timings: timings to plot
80 routine: benchmarked string routine name
81 bench_variant: top-level benchmark variant name
82 title: figure title (generated so far)
83 outpath: output file path (generated so far)
85 y: y-axis values to plot
86 title_final: final figure title
87 outpath_final: file output file path
93 plt
.axes().yaxis
.set_major_formatter(plt
.NullFormatter())
96 title_final
= "%s %s benchmark timings\n%s" % \
97 (routine
, bench_variant
, title
)
98 outpath_final
= os
.path
.join(args
.outdir
, "%s_%s_%s%s" % \
99 (routine
, args
.plot
, bench_variant
, outpath
))
101 return y
, title_final
, outpath_final
104 def plotRelative(timings
, all_timings
, routine
, ifuncs
, bench_variant
,
106 """Plot timing values relative to a chosen ifunc
109 timings: timings to plot
110 all_timings: all collected timings
111 routine: benchmarked string routine name
112 ifuncs: names of ifuncs tested
113 bench_variant: top-level benchmark variant name
114 title: figure title (generated so far)
115 outpath: output file path (generated so far)
117 y: y-axis values to plot
118 title_final: final figure title
119 outpath_final: file output file path
121 # Choose the baseline ifunc
123 baseline
= args
.baseline
.replace("__", "")
127 baseline_index
= ifuncs
.index(baseline
)
129 # Compare timings against the baseline
130 y
= relativeDifference(timings
, all_timings
[baseline_index
])
133 plt
.axhspan(-args
.threshold
, args
.threshold
, color
="lightgray", alpha
=0.3)
134 plt
.axhline(0, color
="k", linestyle
="--", linewidth
=0.4)
135 plt
.ylabel("relative timing (in %)")
136 title_final
= "Timing comparison against %s\nfor %s benchmark, %s" % \
137 (baseline
, bench_variant
, title
)
138 outpath_final
= os
.path
.join(args
.outdir
, "%s_%s_%s%s" % \
139 (baseline
, args
.plot
, bench_variant
, outpath
))
141 return y
, title_final
, outpath_final
144 def plotMax(timings
, routine
, bench_variant
, title
, outpath
):
145 """Plot results as percentage of the maximum ifunc performance.
147 The optimal ifunc is computed on a per-parameter-value basis.
148 Performance is computed as 1/timing.
151 timings: timings to plot
152 routine: benchmarked string routine name
153 bench_variant: top-level benchmark variant name
154 title: figure title (generated so far)
155 outpath: output file path (generated so far)
157 y: y-axis values to plot
158 title_final: final figure title
159 outpath_final: file output file path
161 perf
= np
.reciprocal(timings
)
162 max_perf
= np
.max(perf
, axis
=0)
163 y
= np
.add(100.0, relativeDifference(perf
, max_perf
))
166 plt
.axhline(100.0, color
="k", linestyle
="--", linewidth
=0.4)
167 plt
.ylabel("1/timing relative to max (in %)")
168 title_final
= "Performance comparison against max for %s\n%s " \
169 "benchmark, %s" % (routine
, bench_variant
, title
)
170 outpath_final
= os
.path
.join(args
.outdir
, "%s_%s_%s%s" % \
171 (routine
, args
.plot
, bench_variant
, outpath
))
173 return y
, title_final
, outpath_final
176 def plotThroughput(timings
, params
, routine
, bench_variant
, title
, outpath
):
179 Throughput is computed as the varied parameter value over timing.
182 timings: timings to plot
183 params: varied parameter values
184 routine: benchmarked string routine name
185 bench_variant: top-level benchmark variant name
186 title: figure title (generated so far)
187 outpath: output file path (generated so far)
189 y: y-axis values to plot
190 title_final: final figure title
191 outpath_final: file output file path
193 y
= np
.divide(params
, timings
)
197 plt
.axes().yaxis
.set_major_formatter(plt
.NullFormatter())
199 plt
.ylabel("%s / timing" % args
.key
)
200 title_final
= "%s %s benchmark throughput results\n%s" % \
201 (routine
, bench_variant
, title
)
202 outpath_final
= os
.path
.join(args
.outdir
, "%s_%s_%s%s" % \
203 (routine
, args
.plot
, bench_variant
, outpath
))
204 return y
, title_final
, outpath_final
207 def finishPlot(x
, y
, title
, outpath
, x_scale
, plotted_ifuncs
):
208 """Finish generating current Figure.
214 outpath: output file path
215 x_scale: x-axis scale
216 plotted_ifuncs: names of ifuncs to plot
222 plt
.grid(color
="k", linestyle
=args
.grid
, linewidth
=0.5, alpha
=0.5)
224 for i
in range(len(plotted_ifuncs
)):
225 plt
.plot(x
, y
[i
], marker
=markers
[i
% len(markers
)],
226 label
=plotted_ifuncs
[i
])
228 plt
.legend(loc
="best", fontsize
="small")
229 plt
.savefig("%s_%s.%s" % (outpath
, x_scale
, args
.extension
),
230 format
=args
.extension
, dpi
=args
.resolution
)
238 def plotRecursive(json_iter
, routine
, ifuncs
, bench_variant
, title
, outpath
,
240 """Plot benchmark timings.
243 json_iter: reference to json object
244 routine: benchmarked string routine name
245 ifuncs: names of ifuncs tested
246 bench_variant: top-level benchmark variant name
247 title: figure's title (generated so far)
248 outpath: output file path (generated so far)
249 x_scale: x-axis scale
252 # RECURSIVE CASE: 'variants' array found
253 if "variants" in json_iter
:
254 # Continue recursive search for 'results' array. Record the
255 # benchmark variant (configuration) in order to customize
256 # the title, filename and X-axis scale for the generated figure.
257 for variant
in json_iter
["variants"]:
258 new_title
= "%s%s, " % (title
, variant
["name"])
259 new_outpath
= "%s_%s" % (outpath
, variant
["name"].replace(" ", "_"))
260 new_x_scale
= "log" if variant
["name"] in log_variants
else x_scale
262 plotRecursive(variant
, routine
, ifuncs
, bench_variant
, new_title
,
263 new_outpath
, new_x_scale
)
266 # BASE CASE: 'results' array found
268 timings
= defaultdict(list)
271 for result
in json_iter
["results"]:
272 domain
.append(result
[args
.key
])
273 timings
[result
[args
.key
]].append(result
["timings"])
275 domain
= np
.unique(np
.array(domain
))
278 # Compute geometric mean if there are multple timings for each
280 for parameter
in domain
:
281 averages
.append(gmean(timings
[parameter
]))
283 averages
= np
.array(averages
).transpose()
285 # Choose ifuncs to plot
286 if isinstance(args
.ifuncs
, str):
287 plotted_ifuncs
= ifuncs
289 plotted_ifuncs
= [x
.replace("__", "") for x
in args
.ifuncs
]
291 plotted_indices
= [ifuncs
.index(x
) for x
in plotted_ifuncs
]
292 plotted_vals
= averages
[plotted_indices
,:]
294 # Plotting logic specific to each plot type
295 if args
.plot
== "time":
296 codomain
, title
, outpath
= plotTime(plotted_vals
, routine
,
297 bench_variant
, title
, outpath
)
298 elif args
.plot
== "rel":
299 codomain
, title
, outpath
= plotRelative(plotted_vals
, averages
, routine
,
300 ifuncs
, bench_variant
, title
, outpath
)
301 elif args
.plot
== "max":
302 codomain
, title
, outpath
= plotMax(plotted_vals
, routine
,
303 bench_variant
, title
, outpath
)
304 elif args
.plot
== "thru":
305 codomain
, title
, outpath
= plotThroughput(plotted_vals
, domain
, routine
,
306 bench_variant
, title
, outpath
)
308 # Plotting logic shared between plot types
309 finishPlot(domain
, codomain
, title
, outpath
, x_scale
, plotted_ifuncs
)
313 """Program Entry Point.
316 args: command line arguments (excluding program name)
319 # Select non-GUI matplotlib backend if interactive display is disabled
324 import matplotlib
.pyplot
as plt
328 with
open(args
.schema
, "r") as f
:
329 schema
= json
.load(f
)
331 for filename
in args
.bench
:
334 with
open(filename
, "r") as f
:
337 validator
.validate(bench
, schema
)
339 for function
in bench
["functions"]:
340 bench_variant
= bench
["functions"][function
]["bench-variant"]
341 ifuncs
= bench
["functions"][function
]["ifuncs"]
342 ifuncs
= [x
.replace("__", "") for x
in ifuncs
]
344 plotRecursive(bench
["functions"][function
], function
, ifuncs
,
345 bench_variant
, "", "", args
.logarithmic
)
349 if __name__
== "__main__":
351 parser
= argparse
.ArgumentParser(description
=
352 "Plot string microbenchmark results",
353 formatter_class
=argparse
.ArgumentDefaultsHelpFormatter
)
356 parser
.add_argument("bench", nargs
="+",
357 help="benchmark results file(s) in json format")
359 # Optional parameters
360 parser
.add_argument("-b", "--baseline", type=str,
361 help="baseline ifunc for 'rel' plot")
362 parser
.add_argument("-d", "--display", action
="store_true",
363 help="display figures")
364 parser
.add_argument("-e", "--extension", type=str, default
="png",
365 choices
=["png", "pdf", "svg"],
366 help="output file(s) extension")
367 parser
.add_argument("-g", "--grid", action
="store_const", default
="",
368 const
="-", help="show grid lines")
369 parser
.add_argument("-i", "--ifuncs", nargs
="+", default
="all",
370 help="ifuncs to plot")
371 parser
.add_argument("-k", "--key", type=str, default
="length",
372 help="key to access the varied parameter")
373 parser
.add_argument("-l", "--logarithmic", action
="store_const",
374 default
="linear", const
="log",
375 help="use logarithmic x-axis scale")
376 parser
.add_argument("-o", "--outdir", type=str, default
=os
.getcwd(),
377 help="output directory")
378 parser
.add_argument("-p", "--plot", type=str, default
="time",
379 choices
=["time", "rel", "max", "thru"],
380 help="plot absolute timings, relative timings, " \
381 "performance relative to max, or throughput")
382 parser
.add_argument("-r", "--resolution", type=int, default
=100,
383 help="dpi resolution for the generated figures")
384 parser
.add_argument("-s", "--schema", type=str,
385 default
=os
.path
.join(os
.path
.dirname(
386 os
.path
.realpath(__file__
)),
387 "benchout_strings.schema.json"),
388 help="schema file to validate the results file.")
389 parser
.add_argument("-t", "--threshold", type=int, default
=5,
390 help="threshold to mark in 'rel' graph (in %%)")
391 parser
.add_argument("-v", "--values", action
="store_true",
392 help="show actual values")
394 args
= parser
.parse_args()