| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- #!/usr/bin/python3
- # Plot GNU C Library string microbenchmark output.
- # Copyright (C) 2019-2026 Free Software Foundation, Inc.
- # This file is part of the GNU C Library.
- #
- # The GNU C Library is free software; you can redistribute it and/or
- # modify it under the terms of the GNU Lesser General Public
- # License as published by the Free Software Foundation; either
- # version 2.1 of the License, or (at your option) any later version.
- #
- # The GNU C Library is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- # Lesser General Public License for more details.
- #
- # You should have received a copy of the GNU Lesser General Public
- # License along with the GNU C Library; if not, see
- # <https://www.gnu.org/licenses/>.
- """Plot string microbenchmark results.
- Given a benchmark results file in JSON format and a benchmark schema file,
- plot the benchmark timings in one of the available representations.
- Separate figure is generated and saved to a file for each 'results' array
- found in the benchmark results file. Output filenames and plot titles
- are derived from the metadata found in the benchmark results file.
- """
- import argparse
- from collections import defaultdict
- import json
- import matplotlib as mpl
- import numpy as np
- import os
- import sys
- try:
- import jsonschema as validator
- except ImportError:
- print("Could not find jsonschema module.")
- raise
- # Use pre-selected markers for plotting lines to improve readability
- markers = [".", "x", "^", "+", "*", "v", "1", ">", "s"]
- # Benchmark variants for which the x-axis scale should be logarithmic
- log_variants = {"powers of 2"}
- def gmean(numbers):
- """Compute geometric mean.
- Args:
- numbers: 2-D list of numbers
- Return:
- numpy array with geometric means of numbers along each column
- """
- a = np.array(numbers, dtype=complex)
- means = a.prod(0) ** (1.0 / len(a))
- return np.real(means)
- def relativeDifference(x, x_reference):
- """Compute per-element relative difference between each row of
- a matrix and an array of reference values.
- Args:
- x: numpy matrix of shape (n, m)
- x_reference: numpy array of size m
- Return:
- relative difference between rows of x and x_reference (in %)
- """
- abs_diff = np.subtract(x, x_reference)
- return np.divide(np.multiply(abs_diff, 100.0), x_reference)
- def plotTime(timings, routine, bench_variant, title, outpath):
- """Plot absolute timing values.
- Args:
- timings: timings to plot
- routine: benchmarked string routine name
- bench_variant: top-level benchmark variant name
- title: figure title (generated so far)
- outpath: output file path (generated so far)
- Return:
- y: y-axis values to plot
- title_final: final figure title
- outpath_final: file output file path
- """
- y = timings
- plt.figure()
- if not args.values:
- plt.axes().yaxis.set_major_formatter(plt.NullFormatter())
- plt.ylabel("timing")
- title_final = "%s %s benchmark timings\n%s" % \
- (routine, bench_variant, title)
- outpath_final = os.path.join(args.outdir, "%s_%s_%s%s" % \
- (routine, args.plot, bench_variant, outpath))
- return y, title_final, outpath_final
- def plotRelative(timings, all_timings, routine, ifuncs, bench_variant,
- title, outpath):
- """Plot timing values relative to a chosen ifunc
- Args:
- timings: timings to plot
- all_timings: all collected timings
- routine: benchmarked string routine name
- ifuncs: names of ifuncs tested
- bench_variant: top-level benchmark variant name
- title: figure title (generated so far)
- outpath: output file path (generated so far)
- Return:
- y: y-axis values to plot
- title_final: final figure title
- outpath_final: file output file path
- """
- # Choose the baseline ifunc
- if args.baseline:
- baseline = args.baseline.replace("__", "")
- else:
- baseline = ifuncs[0]
- baseline_index = ifuncs.index(baseline)
- # Compare timings against the baseline
- y = relativeDifference(timings, all_timings[baseline_index])
- plt.figure()
- plt.axhspan(-args.threshold, args.threshold, color="lightgray", alpha=0.3)
- plt.axhline(0, color="k", linestyle="--", linewidth=0.4)
- plt.ylabel("relative timing (in %)")
- title_final = "Timing comparison against %s\nfor %s benchmark, %s" % \
- (baseline, bench_variant, title)
- outpath_final = os.path.join(args.outdir, "%s_%s_%s%s" % \
- (baseline, args.plot, bench_variant, outpath))
- return y, title_final, outpath_final
- def plotMax(timings, routine, bench_variant, title, outpath):
- """Plot results as percentage of the maximum ifunc performance.
- The optimal ifunc is computed on a per-parameter-value basis.
- Performance is computed as 1/timing.
- Args:
- timings: timings to plot
- routine: benchmarked string routine name
- bench_variant: top-level benchmark variant name
- title: figure title (generated so far)
- outpath: output file path (generated so far)
- Return:
- y: y-axis values to plot
- title_final: final figure title
- outpath_final: file output file path
- """
- perf = np.reciprocal(timings)
- max_perf = np.max(perf, axis=0)
- y = np.add(100.0, relativeDifference(perf, max_perf))
- plt.figure()
- plt.axhline(100.0, color="k", linestyle="--", linewidth=0.4)
- plt.ylabel("1/timing relative to max (in %)")
- title_final = "Performance comparison against max for %s\n%s " \
- "benchmark, %s" % (routine, bench_variant, title)
- outpath_final = os.path.join(args.outdir, "%s_%s_%s%s" % \
- (routine, args.plot, bench_variant, outpath))
- return y, title_final, outpath_final
- def plotThroughput(timings, params, routine, bench_variant, title, outpath):
- """Plot throughput.
- Throughput is computed as the varied parameter value over timing.
- Args:
- timings: timings to plot
- params: varied parameter values
- routine: benchmarked string routine name
- bench_variant: top-level benchmark variant name
- title: figure title (generated so far)
- outpath: output file path (generated so far)
- Return:
- y: y-axis values to plot
- title_final: final figure title
- outpath_final: file output file path
- """
- y = np.divide(params, timings)
- plt.figure()
- if not args.values:
- plt.axes().yaxis.set_major_formatter(plt.NullFormatter())
- plt.ylabel("%s / timing" % args.key)
- title_final = "%s %s benchmark throughput results\n%s" % \
- (routine, bench_variant, title)
- outpath_final = os.path.join(args.outdir, "%s_%s_%s%s" % \
- (routine, args.plot, bench_variant, outpath))
- return y, title_final, outpath_final
- def finishPlot(x, y, title, outpath, x_scale, plotted_ifuncs):
- """Finish generating current Figure.
- Args:
- x: x-axis values
- y: y-axis values
- title: figure title
- outpath: output file path
- x_scale: x-axis scale
- plotted_ifuncs: names of ifuncs to plot
- """
- plt.xlabel(args.key)
- plt.xscale(x_scale)
- plt.title(title)
- plt.grid(color="k", linestyle=args.grid, linewidth=0.5, alpha=0.5)
- for i in range(len(plotted_ifuncs)):
- plt.plot(x, y[i], marker=markers[i % len(markers)],
- label=plotted_ifuncs[i])
- plt.legend(loc="best", fontsize="small")
- plt.savefig("%s_%s.%s" % (outpath, x_scale, args.extension),
- format=args.extension, dpi=args.resolution)
- if args.display:
- plt.show()
- plt.close()
- def plotRecursive(json_iter, routine, ifuncs, bench_variant, title, outpath,
- x_scale):
- """Plot benchmark timings.
- Args:
- json_iter: reference to json object
- routine: benchmarked string routine name
- ifuncs: names of ifuncs tested
- bench_variant: top-level benchmark variant name
- title: figure's title (generated so far)
- outpath: output file path (generated so far)
- x_scale: x-axis scale
- """
- # RECURSIVE CASE: 'variants' array found
- if "variants" in json_iter:
- # Continue recursive search for 'results' array. Record the
- # benchmark variant (configuration) in order to customize
- # the title, filename and X-axis scale for the generated figure.
- for variant in json_iter["variants"]:
- new_title = "%s%s, " % (title, variant["name"])
- new_outpath = "%s_%s" % (outpath, variant["name"].replace(" ", "_"))
- new_x_scale = "log" if variant["name"] in log_variants else x_scale
- plotRecursive(variant, routine, ifuncs, bench_variant, new_title,
- new_outpath, new_x_scale)
- return
- # BASE CASE: 'results' array found
- domain = []
- timings = defaultdict(list)
- # Collect timings
- for result in json_iter["results"]:
- domain.append(result[args.key])
- timings[result[args.key]].append(result["timings"])
- domain = np.unique(np.array(domain))
- averages = []
- # Compute geometric mean if there are multiple timings for each
- # parameter value.
- for parameter in domain:
- averages.append(gmean(timings[parameter]))
- averages = np.array(averages).transpose()
- # Choose ifuncs to plot
- if isinstance(args.ifuncs, str):
- plotted_ifuncs = ifuncs
- else:
- plotted_ifuncs = [x.replace("__", "") for x in args.ifuncs]
- plotted_indices = [ifuncs.index(x) for x in plotted_ifuncs]
- plotted_vals = averages[plotted_indices,:]
- # Plotting logic specific to each plot type
- if args.plot == "time":
- codomain, title, outpath = plotTime(plotted_vals, routine,
- bench_variant, title, outpath)
- elif args.plot == "rel":
- codomain, title, outpath = plotRelative(plotted_vals, averages, routine,
- ifuncs, bench_variant, title, outpath)
- elif args.plot == "max":
- codomain, title, outpath = plotMax(plotted_vals, routine,
- bench_variant, title, outpath)
- elif args.plot == "thru":
- codomain, title, outpath = plotThroughput(plotted_vals, domain, routine,
- bench_variant, title, outpath)
- # Plotting logic shared between plot types
- finishPlot(domain, codomain, title, outpath, x_scale, plotted_ifuncs)
- def main(args):
- """Program Entry Point.
- Args:
- args: command line arguments (excluding program name)
- """
- # Select non-GUI matplotlib backend if interactive display is disabled
- if not args.display:
- mpl.use("Agg")
- global plt
- import matplotlib.pyplot as plt
- schema = None
- with open(args.schema, "r") as f:
- schema = json.load(f)
- for filename in args.bench:
- bench = None
- if filename == '-':
- bench = json.load(sys.stdin)
- else:
- with open(filename, "r") as f:
- bench = json.load(f)
- validator.validate(bench, schema)
- for function in bench["functions"]:
- bench_variant = bench["functions"][function]["bench-variant"]
- ifuncs = bench["functions"][function]["ifuncs"]
- ifuncs = [x.replace("__", "") for x in ifuncs]
- plotRecursive(bench["functions"][function], function, ifuncs,
- bench_variant, "", "", args.logarithmic)
- """ main() """
- if __name__ == "__main__":
- parser = argparse.ArgumentParser(description=
- "Plot string microbenchmark results",
- formatter_class=argparse.ArgumentDefaultsHelpFormatter)
- # Required parameter
- parser.add_argument("bench", nargs="+",
- help="benchmark results file(s) in json format, " \
- "and/or '-' as a benchmark result file from stdin")
- # Optional parameters
- parser.add_argument("-b", "--baseline", type=str,
- help="baseline ifunc for 'rel' plot")
- parser.add_argument("-d", "--display", action="store_true",
- help="display figures")
- parser.add_argument("-e", "--extension", type=str, default="png",
- choices=["png", "pdf", "svg"],
- help="output file(s) extension")
- parser.add_argument("-g", "--grid", action="store_const", default="",
- const="-", help="show grid lines")
- parser.add_argument("-i", "--ifuncs", nargs="+", default="all",
- help="ifuncs to plot")
- parser.add_argument("-k", "--key", type=str, default="length",
- help="key to access the varied parameter")
- parser.add_argument("-l", "--logarithmic", action="store_const",
- default="linear", const="log",
- help="use logarithmic x-axis scale")
- parser.add_argument("-o", "--outdir", type=str, default=os.getcwd(),
- help="output directory")
- parser.add_argument("-p", "--plot", type=str, default="time",
- choices=["time", "rel", "max", "thru"],
- help="plot absolute timings, relative timings, " \
- "performance relative to max, or throughput")
- parser.add_argument("-r", "--resolution", type=int, default=100,
- help="dpi resolution for the generated figures")
- parser.add_argument("-s", "--schema", type=str,
- default=os.path.join(os.path.dirname(
- os.path.realpath(__file__)),
- "benchout_strings.schema.json"),
- help="schema file to validate the results file.")
- parser.add_argument("-t", "--threshold", type=int, default=5,
- help="threshold to mark in 'rel' graph (in %%)")
- parser.add_argument("-v", "--values", action="store_true",
- help="show actual values")
- args = parser.parse_args()
- main(args)
|