166 lines
5.8 KiB
Python
166 lines
5.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
common.benchmark — утилиты для замеров времени и построения таблиц/графиков.
|
||
|
||
Используется во всех лабораторных (1–7), чтобы отчёты были единообразными.
|
||
|
||
Пример:
|
||
|
||
from common.benchmark import time_call, make_table, save_speedup_plot
|
||
|
||
elapsed, result = time_call(lambda: task["kernel"](chunk), repeats=1)
|
||
rows = [("последовательно", T1), ("потоки, p=4", T2)]
|
||
print(make_table(rows))
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import statistics
|
||
import time
|
||
from typing import Callable, Dict, List, Sequence, Tuple
|
||
|
||
|
||
def time_call(fn: Callable, args: tuple = (), repeats: int = 3) -> Tuple[float, any]:
|
||
"""Замер времени выполнения fn(*args) с повторами.
|
||
|
||
Возвращает (лучшее_время_сек, результат). Лучшее (минимальное) время —
|
||
стандартная практика: случайные помехи только удлиняют замер.
|
||
"""
|
||
best = float("inf")
|
||
result = None
|
||
for _ in range(repeats):
|
||
t0 = time.perf_counter()
|
||
result = fn(*args)
|
||
best = min(best, time.perf_counter() - t0)
|
||
return best, result
|
||
|
||
|
||
def speedup(T1: float, Tp: float) -> float:
|
||
"""Ускорение S(p) = T(1) / T(p)."""
|
||
if Tp <= 0:
|
||
return 0.0
|
||
return T1 / Tp
|
||
|
||
|
||
def efficiency(speedup_val: float, p: int) -> float:
|
||
"""Эффективность E(p) = S(p) / p (доля, а не проценты)."""
|
||
if p <= 0:
|
||
return 0.0
|
||
return speedup_val / p
|
||
|
||
|
||
def amdahl(speedup_bound: float, p: int) -> float:
|
||
"""Теоретическое ускорение по закону Амдала.
|
||
|
||
speedup_bound — доля последовательной части (0..1), т.е. доля кода,
|
||
которая не распараллеливается.
|
||
"""
|
||
s = max(1e-12, speedup_bound)
|
||
return 1.0 / (s + (1.0 - s) / p)
|
||
|
||
|
||
def make_table(rows: Sequence[Tuple[str, float]], T1: float = None,
|
||
title: str = "Замеры") -> str:
|
||
"""Текстовая таблица: подход | T, c | S(p) | E(p).
|
||
|
||
rows — пары (название, время в секундах). T1 — время последовательного
|
||
запуска (первая строка, если не задано отдельно).
|
||
"""
|
||
if T1 is None:
|
||
T1 = rows[0][1]
|
||
lines = [
|
||
f"{title}",
|
||
f"{'подход':<32} {'T, c':>10} {'S(p)':>8} {'E(p)':>8}",
|
||
"-" * 62,
|
||
]
|
||
for name, t in rows:
|
||
s = speedup(T1, t)
|
||
p = _p_from_name(name)
|
||
e = efficiency(s, p)
|
||
lines.append(f"{name:<32} {t:>10.3f} {s:>8.2f} {e:>8.2f}")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _p_from_name(name: str) -> int:
|
||
"""Пытается вытащить число воркеров из названия ('потоки, p=4' -> 4)."""
|
||
if "p=" in name:
|
||
try:
|
||
return int(name.split("p=")[1].split()[0].strip("),"))
|
||
except ValueError:
|
||
pass
|
||
return 1
|
||
|
||
|
||
def save_speedup_plot(results: Dict[str, List[Tuple[int, float]]], title: str,
|
||
out_path: str, show: bool = False):
|
||
"""График ускорения S(p) для нескольких подходов.
|
||
|
||
results = {"потоки": [(1, 1.0), (2, 1.9), (4, 3.8)],
|
||
"процессы": [(1, 1.0), (2, 1.95), (4, 3.7)]}
|
||
|
||
Для каждого подхода S(p) считается относительно его же T(1) — так
|
||
кривые корректно сравниваются между собой.
|
||
"""
|
||
import matplotlib
|
||
if not show:
|
||
matplotlib.use("Agg")
|
||
import matplotlib.pyplot as plt
|
||
|
||
plt.figure(figsize=(8, 5))
|
||
for name, pts in results.items():
|
||
pts = sorted(pts)
|
||
base = next((t for p, t in pts if p == 1), None)
|
||
if base is None:
|
||
continue
|
||
xs = [p for p, _ in pts]
|
||
ys = [base / t for _, t in pts]
|
||
plt.plot(xs, ys, marker="o", label=name)
|
||
plt.plot(xs, xs, "--", color="gray", label="идеал S(p)=p")
|
||
plt.xlabel("число воркеров p")
|
||
plt.ylabel("ускорение S(p) = T(1)/T(p)")
|
||
plt.title(title)
|
||
plt.legend()
|
||
plt.grid(True, alpha=0.3)
|
||
plt.tight_layout()
|
||
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
||
plt.savefig(out_path, dpi=150)
|
||
if show:
|
||
plt.show()
|
||
plt.close()
|
||
print(f"график сохранён: {out_path}")
|
||
|
||
|
||
def save_bar_plot(results: Dict[str, float], title: str, out_path: str,
|
||
ylabel: str = "время, c", show: bool = False):
|
||
"""Столбчатая диаграмма: подход -> значение (время или ускорение)."""
|
||
import matplotlib
|
||
if not show:
|
||
matplotlib.use("Agg")
|
||
import matplotlib.pyplot as plt
|
||
|
||
names = list(results.keys())
|
||
vals = [results[k] for k in names]
|
||
plt.figure(figsize=(9, 5))
|
||
plt.bar(names, vals, color="steelblue")
|
||
plt.ylabel(ylabel)
|
||
plt.title(title)
|
||
plt.xticks(rotation=20, ha="right")
|
||
plt.grid(True, axis="y", alpha=0.3)
|
||
plt.tight_layout()
|
||
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
||
plt.savefig(out_path, dpi=150)
|
||
if show:
|
||
plt.show()
|
||
plt.close()
|
||
print(f"график сохранён: {out_path}")
|
||
|
||
|
||
def median_time(fn: Callable, args: tuple = (), repeats: int = 5) -> Tuple[float, any]:
|
||
"""Аналог time_call, но по медиане — устойчив к выбросам."""
|
||
times = []
|
||
result = None
|
||
for _ in range(repeats):
|
||
t0 = time.perf_counter()
|
||
result = fn(*args)
|
||
times.append(time.perf_counter() - t0)
|
||
return statistics.median(times), result |