Я попытался добавить параллельное выполнение в свой проект на 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 должны быть быстрее, верно? Я предполагаю, что моя компиляция неверна, но я не могу понять, в чем проблема.
sum += i;
может быть одна машинная инструкция, занимающая около наносекунды. Запуск новых тем занимает гораздо больше времени. Чтобы получить «прибыль», каждый поток должен проделать гораздо больше работы, чтобы вернуть эти инвестиции.
проверьте сборку и сравните две функции
Вам следует использовать не omp_set_num_threads
, а переменную окружения. Из-за этого начальный раздел parallel
, безусловно, бесполезен. Среды выполнения обычно повторно инициализируют параллельный раздел при изменении количества потоков (что означает повторное создание всех потоков).
Потоки не обязательно дают прирост скорости. Они приводят к накладным расходам, и не каждый тип вычислений можно эффективно распараллелить. требует, чтобы дискретные подмножества вычислений были достаточно независимыми от других подмножеств, чтобы их можно было выполнять параллельно (например, если один набор вычислений вычисляет A, а второй набор вычислений опирается на то, что A уже вычислено, то принудительное их распараллеливание неизменно будет занимают больше времени, чем выполнение их последовательно, из-за дополнительных накладных расходов на обработку потоков).
@BoP На самом деле, я ожидаю, что компиляторы даже сгенерируют здесь SIMD-инструкции, поскольку автоматическая векторизация включена по умолчанию в -O3
. На машинах x86-64 компиляторы (по крайней мере, GCC/Clang) по умолчанию используют SSE2, поэтому 4 элемента на инструкцию и, возможно, 2 инструкции на цикл, то есть, возможно, 8 элементов/циклов. -mavx2
или даже -march=native
должно помочь ускорить работу на последних процессорах.
high_resolution_clock
не предназначен для измерения времени на настенных часах (не монотонно). Используйте steady_clock
или еще лучше: omp_get_wtime
.
@Yksisarvinen Clang может это сделать, но GCC пока нет. Кажется, это подтверждает godbolt.org/z/KeG1rbKKq . Однако Clang, похоже, не может использовать математический трюк при использовании OpenMP (вместо этого используется неэффективный цикл).
Я попробовал ваш пример и заметил, что использовался только один поток, даже с 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).
Обе версии демонстрируют неопределенное поведение. А если бы это было не так, я бы ожидал, что компилятор
-O3
оптимизируетsum_serial
доreturn n*(n+1)/2;
и, возможно, сделает то же самое сsum_parallel
, если OpenMP не слишком все испортит.