Files
RandQuik/randquik/benchmark.py
T

134 lines
4.0 KiB
Python

"""Benchmark functions for measuring performance."""
import contextlib
import os
import pathlib
import re
import subprocess
import sys
import time
from randquik.utils import sparse_range
__all__ = ["bench_mode", "run_benchmark"]
def bench_mode(
tcounts: list[int],
io_mode: str,
length: str,
alg: str | None,
bench_file: pathlib.Path | None,
) -> list[tuple[int, float, list[str]]]:
max_repeats = 5
max_time = 0.5 # Quit early if more than 500ms has passed
results = []
# Use a file in current folder for file modes
if "file" in io_mode:
iocmd = ["-o", str(bench_file)]
elif "dry" in io_mode:
iocmd = ["--dry"]
elif "null" in io_mode:
iocmd = ["-o", os.devnull]
else:
raise ValueError(f"Unknown io_mode: {io_mode}")
# Print iocmd at start of row
print(f"{' '.join(iocmd)[:20]:<20}", end="", flush=True)
for workers in tcounts:
speeds = []
worker_start = time.perf_counter()
for rep in range(max_repeats):
if rep > 0 and (time.perf_counter() - worker_start) > max_time:
break
cmd = [
sys.executable,
"-m",
"randquik",
f"-l{length}",
f"-t{workers}",
*([f"-a{alg}"] if alg else []),
*iocmd,
]
try:
proc = subprocess.run(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=False,
)
except KeyboardInterrupt:
print(" Interrupted\n\n", end="", flush=True) # 6 not 8 to account for ^C
print(f">>> {' '.join(cmd)}", file=sys.stderr)
sys.exit(1)
stderr = proc.stderr.decode(errors="ignore")
if proc.returncode != 0:
print(f"{'ERROR':>8}\n\n", end="", flush=True)
print(f">>> {' '.join(cmd)}\n{stderr}", file=sys.stderr)
sys.exit(1)
m2 = re.findall(r"([0-9]+\.[0-9]+)\s+GB/s", stderr)
if m2:
speeds.append(float(m2[-1]))
if speeds:
sorted_speeds = sorted(speeds)
median = sorted_speeds[len(speeds) // 2]
print(f"{median:>8.2f}", end="", flush=True)
results.append((workers, median, iocmd))
else:
print(f"{'---':>8}", end="", flush=True)
print() # newline after row
# Cleanup bench file
if bench_file:
with contextlib.suppress(OSError):
bench_file.unlink()
return results
def run_benchmark(args):
"""Run comprehensive benchmark across all I/O modes."""
# Check if output file already exists
bench_file = pathlib.Path(args.output or "test.dat")
if bench_file.exists():
if not args.output:
raise ValueError(
f"File test.dat already exists. Use -o {bench_file} to benchmark over it or choose another name."
)
bench_file.unlink()
length = args.len or "1G"
max_threads = args.threads if args.threads is not None else os.cpu_count()
all_results = {}
tcounts = sparse_range(max_threads)
# Print header row
print(f"{'randquik':<20}", end="")
for w in tcounts:
print(f"{'-t' + str(w):>8}", end="")
print()
print("-" * (20 + 8 * len(tcounts)))
for io_mode in ["dry", "null", "file"]:
results = bench_mode(tcounts, io_mode, length, alg=args.alg, bench_file=bench_file)
all_results[io_mode] = results
print("-" * (20 + 8 * len(tcounts)))
# Find fastest configuration and RNG speed
gen_speed = max(r[1] for res in all_results.values() for r in res)
best_speed, best_threads, best_iocmd = max(
[(sp, w, iocmd) for w, sp, iocmd in all_results["file"]],
)
threads = f" -t{best_threads}" if best_threads != 1 else ""
print(
f"\n>>> Fastest wrote {best_speed:.2f} GB/s, plain RNG {gen_speed:.0f} GB/s\n"
f"randquik {' '.join(best_iocmd)}{threads}\n"
)