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 шт):

Автор решения: mur

Итоги экспериментов:

  1. #include <omp.h> никому (msvc, clang, MinGW) не нужна, и без неё всё работает.

  1. С конструкцией:
#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 несколько раз выдали мне маленькую разницу непонятно, сейчас уже сложно будет это воспроизвести, но наверно уже и не важно. Важны стабильно воспроизводимые результаты.


  1. Дополнительно поэкспериментировал с распараллеливанием 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(...) нужна, причём персональная в каждом потоке. Для примитивного учебного теста "ускоряет / не ускоряет" приведённый код пойдёт, но для реальной программы его надо усложнять, сейчас он выдаёт много повторяющихся участков, потому что несколько потоков работает с одним и тем же начальным значением.

→ Ссылка