Параллельные вычисления
Параллельные алгоритмы
2008 год. Intel выпускает Core i7 с 4 ядрами. Алгоритмы, написанные для одного ядра, не становятся быстрее. Нужно переписывать. Но как? Параллельные алгоритмы - это не просто 'запустить в нескольких потоках', это принципиально другой способ мышления о вычислениях.
- PostgreSQL parallel query: parallel sort и hash join на 32-ядерных серверах ускоряют аналитические запросы на 10-40x
- CUDA parallel sort в GPU базах данных (DuckDB GPU mode, cuDF от NVIDIA) - 100GB датасет сортируется за секунды
- Python scikit-learn GridSearchCV с n_jobs=-1 использует fork-join для параллельного перебора гиперпараметров - стандартная практика ML
Параллельная сортировка
Merge sort параллелизуется естественно: разделить массив на N/P кусков (P - число ядер), отсортировать каждый параллельно, затем смержить. Проблема: merge фаза - узкое место. Parallel merge sort на 8 ядрах даёт 5-6x ускорение (закон Амдала: sequential merge ~15%).
GPU radix sort (CUDA CUB библиотека) - 10-20x быстрее CPU sort на больших массивах. Используется в базах данных: PostgreSQL 16 добавил parallel sort для hash joins, что ускорило аналитические запросы на 40-60% на серверах с 32+ ядрами.
Почему параллельный merge sort не даёт линейного ускорения при P ядрах?
Parallel Prefix (Scan): строительный блок параллельных алгоритмов
Prefix sum (scan): для массива [a0, a1, ..., an] вычислить [a0, a0+a1, a0+a1+a2, ...]. Наивно - O(n) последовательно. Параллельно - O(log n) шагов, O(n) работы. Это примитив для 10+ алгоритмов: параллельный filter, histogram, radix sort, sparse matrix операции.
Thrust (CUDA C++ библиотека) и CUB предоставляют готовые parallel scan. В CPU мире: Intel TBB parallel_scan, std::inclusive_scan с execution::par (C++17). Spark использует prefix scan для distributed aggregations - shuffle phase.
Почему parallel prefix sum называют 'строительным блоком' параллельных алгоритмов?
Work Stealing: динамическая балансировка нагрузки
Work stealing решает проблему неравномерной нагрузки при fork-join параллелизме. Каждый поток имеет свою deque задач. Когда поток заканчивает задачи - крадёт с хвоста чужой deque. Воровство с хвоста (не головы) минимизирует конкуренцию: владелец работает с головы, воры - с хвоста.
Go runtime scheduler использует work stealing для горутин. Intel TBB, Cilk Plus, Java ForkJoinPool - все реализуют work stealing. Ключевой инсайт: воровать с хвоста означает кражу больших подзадач (которые ещё не разбились), что амортизирует overhead кражи.
Почему поток-вор крадёт задачи с хвоста deque, а владелец берёт с головы?
Fork-Join: модель разделяй и властвуй
Fork-join - паттерн: fork (разделить задачу), execute parallel, join (собрать результаты). Рекурсивное разделение до threshold, затем sequential leaf. Ключевой вопрос: threshold. Слишком маленький - overhead fork > benefit. Слишком большой - плохая балансировка.
Python multiprocessing.Pool и concurrent.futures.ProcessPoolExecutor реализуют fork-join для CPU-intensive задач обходя GIL. NumPy операции (dot, sort) используют OpenMP fork-join внутри C кода. scikit-learn Parallel с n_jobs=-1 запускает fork-join на все CPU.
Почему использование blocking I/O в Java parallelStream() проблематично?
Ключевые идеи
- Parallel sort: разделить на P кусков, отсортировать параллельно, merge - узкое место (закон Амдала).
- Parallel prefix (scan): O(log n) шагов, примитив для 10+ алгоритмов - filter, histogram, radix sort.
- Work stealing: динамическая балансировка через кражу крупных задач с хвоста чужой deque.
Связанные темы
Параллельные алгоритмы строятся на базовых концепциях параллелизма:
- CUDA и GPU параллелизм — Parallel prefix и radix sort - ключевые примитивы GPU алгоритмов, реализованные в CUB/Thrust
- CSP и каналы (Go) — Fan-out/fan-in в Go - практическая реализация fork-join паттерна для I/O задач
Вопросы для размышления
- Как определить оптимальный threshold для рекурсивного fork-join: где грань между overhead разбиения и пользой параллелизма?
- Почему parallel prefix sum имеет O(n) работы при O(log n) шагах, и как это влияет на energy efficiency?
- В каких сценариях sequential алгоритм быстрее параллельного на том же железе?