При использовании openmp в проекте на C++ ускорения не наблюдается

Я попытался добавить параллельное выполнение в свой проект на C++.

Таким образом, на основе этого примера я определил свой app.cpp следующим образом:

#include <chrono>
#include <iostream>
#include <omp.h>

int sum_serial(int n) {
    int sum = 0;
    for (int i = 0; i <= n; ++i) {
        sum += i;
    }
    return sum;
}

// Parallel programming function
int sum_parallel(int n) {
    int sum = 0;
#pragma omp parallel for reduction(+ : sum)
    for (int i = 0; i <= n; ++i) {
        sum += i;
    }
    return sum;
}

int main(int argc, char* argv[]) {
    // Beginning of parallel region
#pragma omp parallel
    { printf("Hello World... from thread = %d\n", omp_get_thread_num()); }
    // Set threads number.
#if defined(_OPENMP)
    omp_set_num_threads(2);
#endif

    {
        const int n = 100000000;

        auto start_time = std::chrono::high_resolution_clock::now();

        int result_serial = sum_serial(n);

        auto end_time = std::chrono::high_resolution_clock::now();

        std::chrono::duration<double> serial_duration = end_time - start_time;

        start_time = std::chrono::high_resolution_clock::now();

        int result_parallel = sum_parallel(n);
        end_time = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> parallel_duration = end_time - start_time;

        std::cout << "Serial result: " << result_serial << std::endl;
        std::cout << "Parallel result: " << result_parallel << std::endl;
        std::cout << "Serial duration: " << serial_duration.count() << " seconds" << std::endl;
        std::cout << "Parallel duration: " << parallel_duration.count() << " seconds" << std::endl;
        std::cout << "Speedup: " << serial_duration.count() / parallel_duration.count()
                  << std::endl;
    }
    return 0;
}

Что меня удивило, так это отсутствие ускорения, фактически параллельное выполнение намного медленнее. Мой результат:

Serial result: 987459712
Parallel result: 987459712
Serial duration: 0.132073 seconds
Parallel duration: 0.645815 seconds
Speedup: 0.204507

Обратите внимание, что мой cmake:

add_executable(wingdesigner app.cpp)
target_compile_features(app PRIVATE cxx_std_17)

add_compile_options(-Wall -O3 -fopenmp)


target_link_libraries(app PUBLIC gomp)

Что не так?

Должно быть, я делаю что-то не так. Я понимаю, что большое количество потоков может привести к едва заметному ускорению, но в данном конкретном случае 2 потока по сравнению с 1 должны быть быстрее, верно? Я предполагаю, что моя компиляция неверна, но я не могу понять, в чем проблема.

Обе версии демонстрируют неопределенное поведение. А если бы это было не так, я бы ожидал, что компилятор -O3 оптимизирует sum_serial до return n*(n+1)/2; и, возможно, сделает то же самое с sum_parallel, если OpenMP не слишком все испортит.

Yksisarvinen 15.05.2024 10:56
sum += i; может быть одна машинная инструкция, занимающая около наносекунды. Запуск новых тем занимает гораздо больше времени. Чтобы получить «прибыль», каждый поток должен проделать гораздо больше работы, чтобы вернуть эти инвестиции.
BoP 15.05.2024 10:57

проверьте сборку и сравните две функции

Raildex 15.05.2024 10:57

Вам следует использовать не omp_set_num_threads, а переменную окружения. Из-за этого начальный раздел parallel, безусловно, бесполезен. Среды выполнения обычно повторно инициализируют параллельный раздел при изменении количества потоков (что означает повторное создание всех потоков).

Jérôme Richard 15.05.2024 11:01

Потоки не обязательно дают прирост скорости. Они приводят к накладным расходам, и не каждый тип вычислений можно эффективно распараллелить. требует, чтобы дискретные подмножества вычислений были достаточно независимыми от других подмножеств, чтобы их можно было выполнять параллельно (например, если один набор вычислений вычисляет A, а второй набор вычислений опирается на то, что A уже вычислено, то принудительное их распараллеливание неизменно будет занимают больше времени, чем выполнение их последовательно, из-за дополнительных накладных расходов на обработку потоков).

Peter 15.05.2024 11:03

@BoP На самом деле, я ожидаю, что компиляторы даже сгенерируют здесь SIMD-инструкции, поскольку автоматическая векторизация включена по умолчанию в -O3. На машинах x86-64 компиляторы (по крайней мере, GCC/Clang) по умолчанию используют SSE2, поэтому 4 элемента на инструкцию и, возможно, 2 инструкции на цикл, то есть, возможно, 8 элементов/циклов. -mavx2 или даже -march=native должно помочь ускорить работу на последних процессорах.

Jérôme Richard 15.05.2024 11:05
high_resolution_clock не предназначен для измерения времени на настенных часах (не монотонно). Используйте steady_clock или еще лучше: omp_get_wtime.
Jérôme Richard 15.05.2024 11:09

@Yksisarvinen Clang может это сделать, но GCC пока нет. Кажется, это подтверждает godbolt.org/z/KeG1rbKKq . Однако Clang, похоже, не может использовать математический трюк при использовании OpenMP (вместо этого используется неэффективный цикл).

Jérôme Richard 15.05.2024 11:17
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
8
105
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я попробовал ваш пример и заметил, что использовался только один поток, даже с OMP.

На самом деле, похоже, что ваш файл CMake неправильный (следует использовать target_compile_options вместо add_compile_options). Я использовал следующее и заметил некоторое ускорение:

project (foobar)
add_executable(app app.cpp)
target_compile_features(app PRIVATE cxx_std_17)
target_compile_options (app PUBLIC -Wall -O3 -fopenmp)
target_link_libraries  (app PUBLIC gomp)

Я также использовал следующий фрагмент (с sin), чтобы иметь «больше» работы:

#include <chrono>
#include <iostream>
#include <omp.h>
#include <cmath>

auto sum_serial(int n) {
    double sum = 0;
    for (int i = 0; i <= n; ++i) {
        sum += sin(i);
    }
    return sum;
}

// Parallel programming function
auto sum_parallel(int n) {
    double sum = 0;
#pragma omp parallel for reduction(+ : sum)
    for (int i = 0; i <= n; ++i) {
        sum += sin(i);
    }
    return sum;
}

int main(int argc, char* argv[]) {
    // Beginning of parallel region
#pragma omp parallel
    { printf("Hello World... from thread = %d\n", omp_get_thread_num()); }
    // Set threads number.
#if defined(_OPENMP)
    omp_set_num_threads(2);
#endif

    {
        const int n = 100000000;

        auto start_time = std::chrono::high_resolution_clock::now();

        auto result_serial = sum_serial(n);

        auto end_time = std::chrono::high_resolution_clock::now();

        std::chrono::duration<double> serial_duration = end_time - start_time;

        start_time = std::chrono::high_resolution_clock::now();

        auto result_parallel = sum_parallel(n);
        end_time = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> parallel_duration = end_time - start_time;

        std::cout << "Serial result    : " << result_serial << std::endl;
        std::cout << "Parallel result  : " << result_parallel << std::endl;
        std::cout << "Serial duration  : " << serial_duration.count() << " seconds" << std::endl;
        std::cout << "Parallel duration: " << parallel_duration.count() << " seconds" << std::endl;
        std::cout << "Speedup          : " << serial_duration.count() / parallel_duration.count() << std::endl;
    }
    return 0;
}

Я получил следующий вывод:

Serial result    : 1.71365
Parallel result  : 1.71365
Serial duration  : 1.08041 seconds
Parallel duration: 0.546919 seconds
Speedup          : 1.97545

Портативный способ связать OpenMP с CMake — find_package(OpenMP REQUIRED), а затем target_link_libraries(app PUBLIC OpenMP::OpenMP_CXX). При этом выбирается флаг OpenMP, специфичный для компилятора (-qopenmp/-fopenmp/...) и связывается с соответствующей библиотекой (которая не всегда является gomp).

Joachim 16.05.2024 10:52

Другие вопросы по теме