6.7 KiB
Лабораторная работа 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
собирает статистику по функциям: число вызовов, суммарное время, время без
вложенных вызовов.
import cProfile
cProfile.run("main()", sort="cumulative")
Интерпретация: колонка cumtime — общее время функции (с вызовами),
totime — собственное. Первые строки обычно сразу показывают узкое место.
Задание
Ваша задача и параметры — по варианту (common/варианты.md), доступ:
from common.tasks import get_variant, TASKS
v = get_variant(13) # ваш номер
task = TASKS[v["task_name"]]
data = task["build"](v["cpu_params"])
Заполните solution.py (интерфейсы менять нельзя):
run_cpu_sequential(task_name, params, parts=4)— выполняет задачу последовательно:build→ разбиение наpartsкусков →kernelдля каждого куска в цикле →combine→ вернутьchecksum. Куски обязательны: в следующих лабораторных те же куски уйдут воркерам.run_io_sequential(items, delay)— для каждого элемента вызываетcommon.tasks.io_fetch(item, delay)и собирает результаты в список (в порядке возрастания item).main()— запускает обе части на параметрах своего варианта, выводит таблицу времен (common.benchmark.make_table) и значения checksum.
Измерения для отчёта
- CPU-часть: T(1) при разбиении на 1, 2, 4 куска (убедитесь, что checksum одинаков и время почти не меняется — разбиение не должно менять работу).
- I/O-часть: суммарное время для items из варианта; сравните
perf_counterиprocess_time— увидите, сколько времени машина спит. - Профилирование CPU-части:
cProfile.run(...), топ-5 функций по времени. Укажите в отчёте, какая функция — узкое место.
Контрольные вопросы
- Чем CPU-bound задача отличается от I/O-bound? Какая у вас?
- Почему для замеров нужен
perf_counter, а неtime.time()? - Что показывает
cumtimeиtototimeв выводе cProfile? - Почему 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). Здесь их всего две: замеры в
отладчике и замеры без повторов.