Init
This commit is contained in:
commit
c394677d07
59 files changed
+5149
No files matched your search
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: perf_counter против time.time и process_time.
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab01_sequential/examples/01_perf_counter_demo.py
|
||||
|
||||
Показывает:
|
||||
- почему time.time() для замеров плох (не монотонные часы);
|
||||
- как perf_counter и process_time диагностируют I/O-bound задачу.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
|
||||
def demo_time_vs_perf():
|
||||
print("--- time.time() против time.perf_counter() ---")
|
||||
t1 = time.time()
|
||||
p1 = time.perf_counter()
|
||||
time.sleep(0.05)
|
||||
t2 = time.time()
|
||||
p2 = time.perf_counter()
|
||||
print(f"time.time(): {(t2 - t1) * 1000:8.2f} мс (точность зависит от ОС)")
|
||||
print(f"time.perf_counter():{(p2 - p1) * 1000:8.2f} мс (монотонные, наносекундное разрешение)")
|
||||
|
||||
|
||||
def demo_cpu_vs_io():
|
||||
print("\n--- CPU-bound против I/O-bound: perf_counter vs process_time ---")
|
||||
|
||||
# CPU-bound: считаем
|
||||
t0 = time.perf_counter()
|
||||
c0 = time.process_time()
|
||||
s = 0.0
|
||||
for i in range(3_000_000):
|
||||
s += i ** 0.5
|
||||
cpu_wall = time.perf_counter() - t0
|
||||
cpu_proc = time.process_time() - c0
|
||||
|
||||
# I/O-bound: ждём (имитация сетевых запросов)
|
||||
t0 = time.perf_counter()
|
||||
c0 = time.process_time()
|
||||
for _ in range(10):
|
||||
time.sleep(0.05) # «ожидание ответа сервера»
|
||||
io_wall = time.perf_counter() - t0
|
||||
io_proc = time.process_time() - c0
|
||||
|
||||
print(f"{'':14}{'настенное, c':>14}{'процессорное, c':>18}{'доля сна':>10}")
|
||||
print(f"{'CPU-bound':14}{cpu_wall:>14.3f}{cpu_proc:>18.3f}"
|
||||
f"{100 * (1 - cpu_proc / cpu_wall):>9.0f}%")
|
||||
print(f"{'I/O-bound':14}{io_wall:>14.3f}{io_proc:>18.3f}"
|
||||
f"{100 * (1 - io_proc / io_wall):>9.0f}%")
|
||||
print("\nВывод: у I/O-задачи процессор почти всё время спит —")
|
||||
print("именно поэтому потоки/asyncio могут её ускорить (лабы 2 и 6).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo_time_vs_perf()
|
||||
demo_cpu_vs_io()
|
||||
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: разбиение работы на куски не должно менять результат.
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab01_sequential/examples/02_split_check_demo.py
|
||||
|
||||
Ключевая идея курса: задача формулируется как split -> kernel -> combine ->
|
||||
checksum. Разбиение — это подготовка к параллелизму: последовательный запуск
|
||||
кусков обязан давать тот же checksum, что и один большой кусок.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
from common.tasks import TASKS, smoke_params
|
||||
|
||||
|
||||
def run(task_name: str, parts: int):
|
||||
"""Последовательный запуск задачи из `parts` кусков."""
|
||||
task = TASKS[task_name]
|
||||
params = smoke_params(task_name)
|
||||
data = task["build"](params)
|
||||
chunks = task["split"](data, parts)
|
||||
partials = [task["kernel"](ch) for ch in chunks]
|
||||
raw = task["combine"](partials)
|
||||
return task["checksum"](raw)
|
||||
|
||||
|
||||
def main():
|
||||
print(f"{'задача':<12} {'parts=1':>18} {'parts=2':>18} {'parts=4':>18} совпадает")
|
||||
for name in ("pi_mc", "integrate", "nqueens", "blur", "hashing"):
|
||||
v1 = run(name, 1)
|
||||
v2 = run(name, 2)
|
||||
v4 = run(name, 4)
|
||||
# float-результаты (π, интеграл) сравниваем с допуском: при разбиении
|
||||
# меняется порядок суммирования, меняются последние биты мантиссы
|
||||
def same(a, b):
|
||||
if isinstance(a, float):
|
||||
return abs(a - b) <= 1e-6 * max(1.0, abs(a))
|
||||
return a == b
|
||||
same12 = same(v1, v2)
|
||||
same14 = same(v1, v4)
|
||||
marker = "True" if (same12 and same14) else "False <-- ОШИБКА"
|
||||
print(f"{name:<12} {v1:>18.6f} {v2:>18.6f} {v4:>18.6f} {marker}")
|
||||
print("\nЕсли checksum зависит от разбиения — параллельная версия будет")
|
||||
print("давать «случайный» результат. Это ошибка, а не особенность.")
|
||||
print("(Float-задачи сравниваются с допуском — см. код демо.)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: cProfile — где программа проводит время.
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab01_sequential/examples/03_cprofile_demo.py
|
||||
|
||||
На глаз две реализации выглядят одинаково. Профилировщик показывает,
|
||||
что наивная transpose-версия медленнее — из-за плохой локальности памяти.
|
||||
"""
|
||||
import cProfile
|
||||
import os
|
||||
import pstats
|
||||
import sys
|
||||
import io
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
N = 250
|
||||
|
||||
|
||||
def matmul_rows(A, B):
|
||||
"""Доступ к B по строкам: b[k] читается целиком (хорошо для кэша)."""
|
||||
C = []
|
||||
for i in range(N):
|
||||
Ai, row = A[i], []
|
||||
for j in range(N):
|
||||
s = 0.0
|
||||
for k in range(N):
|
||||
s += Ai[k] * B[k][j] # B[k] — колонка, каждый раз новая строка
|
||||
row.append(s)
|
||||
C.append(row)
|
||||
return C
|
||||
|
||||
|
||||
def main():
|
||||
rng_a = [[float(i + j) for j in range(N)] for i in range(N)]
|
||||
rng_b = [[float(i - j) for j in range(N)] for i in range(N)]
|
||||
|
||||
profiler = cProfile.Profile()
|
||||
profiler.enable()
|
||||
C = matmul_rows(rng_a, rng_b)
|
||||
profiler.disable()
|
||||
|
||||
stream = io.StringIO()
|
||||
stats = pstats.Stats(profiler, stream=stream)
|
||||
stats.sort_stats("cumulative").print_stats(8)
|
||||
print(stream.getvalue())
|
||||
print(f"checksum = {sum(sum(r) for r in C):.3f}")
|
||||
print("Смотрим tototime: почти всё время — во внутреннем цикле matmul_rows.")
|
||||
print("Это и есть узкое место, которое будут распараллеливать лабы 3–4.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Лабораторная работа 1: последовательное вычисление и профилирование.
|
||||
|
||||
Заполните функции ниже. ИНТЕРФЕЙСЫ МЕНЯТЬ НЕЛЬЗЯ — по ним работают
|
||||
автотесты (tests/test_lab01.py).
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab01_sequential/solution.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from common.benchmark import make_table, time_call
|
||||
from common.tasks import TASKS, get_variant, io_fetch
|
||||
|
||||
# TODO: впишите свой номер в журнале (1..20)
|
||||
VARIANT_NUMBER = 0
|
||||
|
||||
|
||||
def run_cpu_sequential(task_name: str, params: dict, parts: int = 4):
|
||||
"""Последовательное выполнение задачи из `parts` кусков.
|
||||
|
||||
Возвращает (checksum, elapsed_seconds).
|
||||
|
||||
Схема:
|
||||
build -> split -> kernel (по очереди!) -> combine -> checksum
|
||||
"""
|
||||
task = TASKS[task_name]
|
||||
# TODO: реализуйте по схеме выше и замерьте время perf_counter-ом
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def run_io_sequential(items: int, delay: float = 0.05):
|
||||
"""Последовательное выполнение I/O-нагрузки.
|
||||
|
||||
Для каждого item из range(items) вызывает io_fetch(item, delay).
|
||||
Возвращает (список_результатов, elapsed_seconds).
|
||||
Результаты — в порядке возрастания item.
|
||||
"""
|
||||
# TODO: цикл по items, сбор результатов, замер времени
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def main():
|
||||
v = get_variant(VARIANT_NUMBER)
|
||||
print(f"Вариант {v['variant']}: {v['describe']}")
|
||||
print(f"I/O-нагрузка: {v['io']['items']} запросов по {v['io']['delay']} c\n")
|
||||
|
||||
# --- CPU-часть: T(1), T(2), T(4) --------------------------------------
|
||||
rows, checksums = [], []
|
||||
for parts in (1, 2, 4):
|
||||
t, (cs, _) = time_call(run_cpu_sequential,
|
||||
(v["task_name"], v["cpu_params"], parts))
|
||||
rows.append((f"CPU, {parts} кусков", t))
|
||||
checksums.append(cs)
|
||||
print(make_table(rows, title="CPU-часть (последовательно)"))
|
||||
|
||||
def same(a, b):
|
||||
# float-результаты сравниваем с допуском (порядок суммирования!)
|
||||
if isinstance(a, float) or isinstance(b, float):
|
||||
return abs(a - b) <= 1e-6 * max(1.0, abs(a))
|
||||
return a == b
|
||||
|
||||
ok = all(same(checksums[0], c) for c in checksums[1:])
|
||||
print(f"checksum совпадает при разном разбиении: {ok}\n")
|
||||
|
||||
# --- I/O-часть ----------------------------------------------------------
|
||||
results, t_io = run_io_sequential(v["io"]["items"], v["io"]["delay"])
|
||||
t0 = time.perf_counter()
|
||||
c0 = time.process_time()
|
||||
run_io_sequential(v["io"]["items"], v["io"]["delay"])
|
||||
wall = time.perf_counter() - t0
|
||||
proc = time.process_time() - c0
|
||||
print(f"I/O-часть: {len(results)} запросов за {t_io:.3f} c")
|
||||
print(f" настенное время (perf_counter): {wall:.3f} c")
|
||||
print(f" процессорное время (process_time): {proc:.3f} c")
|
||||
print(f" доля ожидания: {100 * (1 - proc / wall):.0f}%")
|
||||
print(f" потенциал ускорения потоками (лаба 2): x{t_io / v['io']['delay']:.1f}\n")
|
||||
|
||||
# --- Профилирование ------------------------------------------------------
|
||||
import cProfile
|
||||
import io as _io
|
||||
import pstats
|
||||
|
||||
print("--- cProfile CPU-части (топ-5 по времени) ---")
|
||||
profiler = cProfile.Profile()
|
||||
profiler.enable()
|
||||
run_cpu_sequential(v["task_name"], v["cpu_params"], parts=4)
|
||||
profiler.disable()
|
||||
stream = _io.StringIO()
|
||||
pstats.Stats(profiler, stream=stream).sort_stats("cumulative").print_stats(5)
|
||||
print(stream.getvalue())
|
||||
# TODO: выпишите в отчёт, какая функция — узкое место
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,115 @@
|
||||
# Лабораторная работа 1. Последовательное вычисление и профилирование
|
||||
|
||||
**Время на работу:** 2 пары.
|
||||
|
||||
## Цель работы
|
||||
|
||||
Создать базовую линию: последовательную реализацию своей задачи и её замеры.
|
||||
Без неё невозможно показать ускорение в следующих лабораторных — нечего будет
|
||||
ускорять.
|
||||
|
||||
## Теория
|
||||
|
||||
### Последовательное вычисление
|
||||
|
||||
Программа выполняет операцию за операцией, в одном потоке управления. Это
|
||||
простейшая и самая предсказуемая модель: результат не зависит от порядка
|
||||
выполнения, время выполнения = сумма времён всех операций. Все дальнейшие
|
||||
лабораторные сравниваются с этой базой.
|
||||
|
||||
### Два типа нагрузки
|
||||
|
||||
**CPU-bound** — программа ограничена скоростью процессора: считает, шифрует,
|
||||
сортирует. Время определяется числом элементарных операций.
|
||||
|
||||
**I/O-bound** — программа ограничена ожиданием: сеть, диск, база данных.
|
||||
Время определяется временем ожидания ответов, процессор при этом простаивает.
|
||||
|
||||
Отличить просто: если удалить «полезную работу», а программа всё равно
|
||||
медленная — это I/O-bound; если быстрая — CPU-bound. Тип нагрузки определяет,
|
||||
какая модель параллелизма поможет (потоки — для I/O, процессы — для CPU,
|
||||
async — для I/O в больших количествах).
|
||||
|
||||
### Время: настенное и процессорное
|
||||
|
||||
- `time.perf_counter()` — «настенные» часы: сколько времени прошло реально
|
||||
(включая сон и ожидание). Ими меряют всё в этом курсе.
|
||||
- `time.process_time()` — только время, потраченное процессором.
|
||||
Разница между ними для I/O-задачи огромна — это её диагноз.
|
||||
|
||||
Обе функции монотонные (не откатываются назад, как `time.time()` при
|
||||
переводе часов), имеют наносекундное разрешение — всегда используйте их, а не
|
||||
`time.time()`.
|
||||
|
||||
### Профилирование
|
||||
|
||||
Профилировщик отвечает на вопрос «где программа проводит время». `cProfile`
|
||||
собирает статистику по функциям: число вызовов, суммарное время, время без
|
||||
вложенных вызовов.
|
||||
|
||||
```python
|
||||
import cProfile
|
||||
cProfile.run("main()", sort="cumulative")
|
||||
```
|
||||
|
||||
Интерпретация: колонка `cumtime` — общее время функции (с вызовами),
|
||||
`totime` — собственное. Первые строки обычно сразу показывают узкое место.
|
||||
|
||||
## Задание
|
||||
|
||||
Ваша задача и параметры — по варианту (`common/варианты.md`), доступ:
|
||||
|
||||
```python
|
||||
from common.tasks import get_variant, TASKS
|
||||
v = get_variant(13) # ваш номер
|
||||
task = TASKS[v["task_name"]]
|
||||
data = task["build"](v["cpu_params"])
|
||||
```
|
||||
|
||||
Заполните `solution.py` (интерфейсы менять нельзя):
|
||||
|
||||
1. **`run_cpu_sequential(task_name, params, parts=4)`** — выполняет задачу
|
||||
последовательно: `build` → разбиение на `parts` кусков → `kernel` для
|
||||
каждого куска в цикле → `combine` → вернуть `checksum`. Куски
|
||||
обязательны: в следующих лабораторных те же куски уйдут воркерам.
|
||||
2. **`run_io_sequential(items, delay)`** — для каждого элемента вызывает
|
||||
`common.tasks.io_fetch(item, delay)` и собирает результаты в список
|
||||
(в порядке возрастания item).
|
||||
3. **`main()`** — запускает обе части на параметрах своего варианта, выводит
|
||||
таблицу времен (`common.benchmark.make_table`) и значения checksum.
|
||||
|
||||
### Измерения для отчёта
|
||||
|
||||
- CPU-часть: T(1) при разбиении на 1, 2, 4 куска (убедитесь, что checksum
|
||||
одинаков и время почти не меняется — разбиение не должно менять работу).
|
||||
- I/O-часть: суммарное время для items из варианта; сравните
|
||||
`perf_counter` и `process_time` — увидите, сколько времени машина спит.
|
||||
- Профилирование CPU-части: `cProfile.run(...)`, топ-5 функций по времени.
|
||||
Укажите в отчёте, какая функция — узкое место.
|
||||
|
||||
## Контрольные вопросы
|
||||
|
||||
1. Чем CPU-bound задача отличается от I/O-bound? Какая у вас?
|
||||
2. Почему для замеров нужен `perf_counter`, а не `time.time()`?
|
||||
3. Что показывает `cumtime` и `tototime` в выводе cProfile?
|
||||
4. Почему checksum не должен зависеть от числа кусков? Что это проверяет?
|
||||
|
||||
## Что сдаётся
|
||||
|
||||
`solution.py` + отчёт по шаблону: таблицы T(1/2/4 кусков), время I/O-части,
|
||||
сравнение perf_counter/process_time, топ-5 из cProfile, анализ.
|
||||
|
||||
## Критерии оценки
|
||||
|
||||
| Пункт | Баллы |
|
||||
|---|---|
|
||||
| `run_cpu_sequential` работает, checksum совпадает при разном разбиении | 2 |
|
||||
| `run_io_sequential` работает | 1 |
|
||||
| Замеры и сравнение perf_counter/process_time | 2 |
|
||||
| Профилирование и вывод об узком месте | 2 |
|
||||
| Отчёт: таблицы, анализ, ответы на вопросы | 3 |
|
||||
|
||||
## Типичные ошибки
|
||||
|
||||
См. `common/типичные_ошибки.md` (пп. 8, 9). Здесь их всего две: замеры в
|
||||
отладчике и замеры без повторов.
|
||||
Reference in new issue
Block a user