#!/usr/bin/env python3 # ===----------------------------------------------------------------------===## # # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # # ===----------------------------------------------------------------------===## import argparse import glob import pathlib import subprocess import sys def main(): parser = argparse.ArgumentParser( prog="run-spec-benchmark", description="Build and run a SPEC CPU benchmark using a given compiler, then output results in LNT flat format.", ) parser.add_argument( "--spec-dir", type=str, required=True, help="Path to the SPEC CPU installation directory.", ) parser.add_argument( "--benchmark", type=str, required=True, help="Name of the SPEC benchmark to run (e.g. 520.omnetpp_r).", ) parser.add_argument( "--temp-dir", type=str, required=True, help="Temporary directory for build artifacts and results.", ) parser.add_argument( "--clean", action="store_true", help="Clean up build artifacts after running. Useful to save disk space when running multiple benchmarks.", ) parser.add_argument( "compiler_and_flags", nargs="+", help="The C++ compiler followed by all flags (passed after --).", ) args = parser.parse_args() spec_dir = pathlib.Path(args.spec_dir) temp_dir = pathlib.Path(args.temp_dir) utils_dir = pathlib.Path(__file__).resolve().parent cxx_and_flags = " ".join(args.compiler_and_flags) # Generate the SPEC configuration file. We force C++17 since SPEC doesn't # compile with newer standards. temp_dir.mkdir(parents=True, exist_ok=True) spec_config = temp_dir / "spec-config.cfg" spec_config.write_text( f"""\ default: ignore_errors = 1 iterations = 1 label = spec-stdlib log_line_width = 4096 makeflags = --jobs=8 mean_anyway = 1 output_format = csv preenv = 0 reportable = 0 tune = base copies = 1 threads = 1 CC = cc -O3 -std=c18 -Wno-implicit-function-declaration CXX = {cxx_and_flags} -std=c++17 -w # we don't care about warnings in SPEC CC_VERSION_OPTION = --version CXX_VERSION_OPTION = --version EXTRA_PORTABILITY = -DSPEC_NO_CXX17_SPECIAL_MATH_FUNCTIONS # libc++ doesn't implement the special math functions yet """ ) runcpu = str(spec_dir / "bin" / "runcpu") config_flag = f"--config={spec_config}" output_root_flag = f"--output_root={temp_dir}" # Build the benchmark. subprocess.check_call([runcpu, config_flag, "--action=build", output_root_flag, args.benchmark]) # Run the benchmark. Wrap in /usr/bin/time to collect resource usage. time_output = temp_dir / "time.txt" subprocess.check_call(["/usr/bin/time", "-l", "-o", str(time_output), runcpu, config_flag, "--action=run", "--size=ref", output_root_flag, args.benchmark]) # Validate results: runcpu doesn't exit with an error code on build/run errors, so we # need to check the CSV output for CE (Compilation Error) or RE (Runtime Error). result_dir = temp_dir / "result" csv_files = sorted(glob.glob(str(result_dir / "*.refrate.csv"))) log_files = sorted(glob.glob(str(result_dir / "*.log"))) if not csv_files: for log_file in log_files: print(pathlib.Path(log_file).read_text(), file=sys.stderr) print("No SPEC CSV result files found -- this indicates a SPEC error.", file=sys.stderr) sys.exit(1) # Check for compilation or runtime errors in the results. status_check = subprocess.run([str(utils_dir / "parse-spec-results"), "--extract=Base Status", "--keep-failed"] + csv_files, capture_output=True, text=True,) if status_check.returncode != 0: for log_file in log_files: print(pathlib.Path(log_file).read_text(), file=sys.stderr) print("Failed to parse SPEC results.", file=sys.stderr) sys.exit(1) for line in status_check.stdout.splitlines(): if "CE" in line or "RE" in line: for log_file in log_files: print(pathlib.Path(log_file).read_text(), file=sys.stderr) print(f"SPEC benchmark {args.benchmark} had a compilation or runtime error.", file=sys.stderr) sys.exit(1) # Parse results into LNT format and print to stdout. benchmark_name = args.benchmark.replace(".", "_") spec_results = subprocess.run([str(utils_dir / "parse-spec-results"), "--output-format=lnt"] + csv_files, capture_output=True, text=True, check=True) sys.stdout.write(spec_results.stdout) time_results = subprocess.run([str(utils_dir / "parse-time-output"), str(time_output), f"--benchmark={benchmark_name}", "--extract", "instructions", "max_rss", "cycles", "peak_memory"], capture_output=True, text=True, check=True) sys.stdout.write(time_results.stdout) # Clean up build artifacts since they can be very large. if args.clean: benchspec_dir = temp_dir / "benchspec" if benchspec_dir.exists(): import shutil shutil.rmtree(benchspec_dir) if __name__ == "__main__": main()