OpenMP на легковесных участках кода
Я пока только разбираюсь с OpenMP.
Вижу пользу только если цикл с #pragma omp parallel for
содержит достаточно тяжёлый код, тогда польза от параллельности перевешивает накладные расходы на многопоточность. Влияния количества повторов в цикле не заметил.
Однако здесь красиво так утверждается что код:
double parallel_max(double* A, int size)
{
int maxval = INT_MIN;
#pragma omp parallel for reduction(max : maxval)
for (i = 0; i < size; ++i)
if (A[i] > maxval) maxval = A[i];
return maxval;
}
оптимизирует производительность.
По-быстрому слепил тест:
#ifndef NOMINMAX
#define NOMINMAX // отключить макросы min / max чтобы они не конфликтовали с std::min / std::max - нужен для opencl.hpp
#endif
// Файлы заголовков среды выполнения C
#include <stdlib.h>
#include <windows.h>
#include <iostream>
#include <fstream>
#include <vector>
#include <chrono>
#include <thread>
#include <format>
#include <omp.h>
//------------------------------------------------------------------------------
// Заполнить вектор псевдослучайными числами в диапазоне от -max_rnd до max_rnd
template<typename Type>
void fill_rand(std::vector<Type> &A, Type max_rnd = 100)
{
for (size_t i = 0; i < A.size(); i++)
A[i] = static_cast<Type>(rand()) / static_cast <Type> (RAND_MAX / (2 * max_rnd)) - max_rnd;
};
//==============================================================================
int main()
{
using namespace std::chrono;
std::this_thread::sleep_for(100ms); // "Стабилизация" системы перед замером производительности
time_point<steady_clock> time_start;
long long time_min; // минимальное время в серии тестовых замеров
double maxval;
size_t n = 100'000;
std::vector<double> A(n);
fill_rand(A);
int n_tests = 1;
time_min = std::numeric_limits<long long>::max(); // сбросить минимальное время перед новой серией замеров
SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS);
for (int i = 0; i < n_tests; i++)
{
time_start = steady_clock::now();
maxval = -std::numeric_limits<double>::max();
// #pragma omp parallel for reduction(max : maxval)
for (size_t i = 0; i < A.size(); i++)
if (A[i] > maxval) maxval = A[i];
time_min = std::min(time_min, duration_cast<nanoseconds>(steady_clock::now() - time_start).count());
};
SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS);
std::cout << std::format("n = {}, maxval = {:.2f}, time of test - {} ns.\n", n, maxval, time_min);
system("pause");
return 0;
};
И вижу, что при n = 100'000
и менее #pragma omp parallel for reduction(max : maxval)
существенно так замедляет, а не ускоряет. При n = 100'000'000
что с #pragma omp
, что без неё время одинаково (с поправкой на естественные колебания замеров).
И ни при каком n
я не получил ускорения от OpenMP.
Это я чего-то не понимаю?
Или всё таки OpenMP даёт выигрыш только при распараллеливании тяжёлого функционала, а примеры от Microsoft фейковые?
Примеры замеров:
n = 100'000
:
- 10'700 ns без
#pragma omp
- 397'600 ns с
#pragma omp
- сильное замедление вместо ускорения
n = 100'000'000
:
- ~30 милисекунд в обоих вариантах.
Естественно значения ориентировочные и "пляшут" при повторных запусках теста, однако порядок и соотношение стабильно такие.
Windows 10, компиляторы от msys2:
- clang с ключами -std=c++23 -O2 -mavx2 -fopenmp
- MinGW с ключами -std=c++23 -Ofast -march=x86-64-v3 -fopenmp
Ответы (1 шт):
Итоги экспериментов:
- #include <omp.h> никому (msvc, clang, MinGW) не нужна, и без неё всё работает.
- С конструкцией:
#pragma omp parallel for reduction(max : maxval)
for (size_t i = 0; i < A.size(); i++)
if (A[i] > maxval) maxval = A[i];
msvc и clang выдают заметное ускорение с OpenMP:
- n = 300'000, pragma - 30 mks.
- n = 300'000, no pragma - 140 mks.
А MinGW, с которым я до этого больше всего экспериментировал, выдаёт:
- n = 300'000, pragma - 25 mks.
- n = 300'000, no pragma - 30 mks.
причём он даже без pragma
и без ключа -fopenmp
выдаёт результат как у msvc и clang с OpenMP.
Похоже этот код в MinGW попадает под какие-то внутренние алгоритмы распараллеливания.
Поэтому я и либо не видел разницы, либо видел обратный эффект, потому что внутреннее распараллеливание MinGW похоже конфликтует с OpenMP, поэтому замедление даже при больших n
.
Почему msvc и clang несколько раз выдали мне маленькую разницу непонятно, сейчас уже сложно будет это воспроизвести, но наверно уже и не важно. Важны стабильно воспроизводимые результаты.
- Дополнительно поэкспериментировал с распараллеливанием fill_rand:
#pragma omp parallel for
for (size_t i = 0; i < A.size(); i++)
A[i] = static_cast<double>(rand()) / static_cast <double> (RAND_MAX / (2 * max_rnd)) - max_rnd;
На нём все три компилятора ведут себя одинаково, логично и предсказуемо - ускоряются с помощью pragma
, причём начиная уже с n = 200
:
- n = 300, pragma - 2000 ns.
- n = 300, no pragma - 7100 ns.
Только здесь уже инициализация srand(...) нужна, причём персональная в каждом потоке. Для примитивного учебного теста "ускоряет / не ускоряет" приведённый код пойдёт, но для реальной программы его надо усложнять, сейчас он выдаёт много повторяющихся участков, потому что несколько потоков работает с одним и тем же начальным значением.