Можно ли безопасно использовать OpenMP с С++ 11?
В стандарте OpenMP рассматриваются только С++ 98 (ISO/IEC 14882: 1998). Это означает, что нет стандартного поддерживающего использования OpenMP в С++ 03 или даже С++ 11. Таким образом, любая программа, использующая С++ > 98 и OpenMP, работает вне стандартов, подразумевая, что даже если она работает при определенных условиях, она вряд ли будет переносной, но определенно никогда не будет гарантирована.
Ситуация еще хуже с С++ 11 с собственной поддержкой многопоточности, которая, скорее всего, столкнется с OpenMP для определенных реализаций.
Итак, насколько безопасно использовать OpenMP с С++ 03 и С++ 11?
Можно ли безопасно использовать многопоточность С++ 11, а также OpenMP в одной и той же программе, но не чередуя их (т.е. никакой оператор OpenMP в любом коде, переданном на параллельные функции С++ 11, и не С++ 11 concurrency в потоках, порожденных OpenMP)?
Меня особенно интересует ситуация, когда я сначала вызываю некоторый код с помощью OpenMP, а затем другой код с использованием С++ 11 concurrency в тех же структурах данных.
Ответы
Ответ 1
Уолтер, я считаю, что я не только рассказал вам текущее состояние вещей в что другие обсуждения, но также предоставил вам информацию непосредственно из источника (т.е. из моего коллега, который является частью комитета OpenMP Language).
OpenMP был разработан как облегченное параллельное дополнение к FORTRAN и C, позднее расширенное к идиомам С++ (например, параллельные петли над итераторами с произвольным доступом) и к задаче parallelism с введением явных задач. Он предназначен как переносимый как можно больше платформ и обеспечивающий практически ту же функциональность на всех трех языках. Его модель исполнения довольно проста: однопоточное приложение создает команды потоков в параллельных областях, запускает некоторые вычислительные задачи внутри и затем объединяет команды в последовательное исполнение. Каждый поток из параллельной команды может позже разветкить собственную команду, если включен вложенный parallelism.
Поскольку основное использование OpenMP в High Performance Computing (в конце концов, его модель директивы и исполнения была заимствована из High Performance Fortran), основной целью любой реализации OpenMP является эффективность, а не совместимость с другими парадигмами потоков. На некоторых платформах эффективная реализация может быть достигнута только в том случае, если время выполнения OpenMP является единственным, контролирующим потоки процесса. Также есть некоторые аспекты OpenMP, которые могут плохо воспроизводиться с другими конструкциями потоковой передачи, например, ограничение количества потоков, заданных OMP_THREAD_LIMIT
при использовании двух или более параллельных параллельных областей.
Так как сам стандарт OpenMP не запрещает строго использовать другие парадигмы потоков, но ни стандартизирует совместимость с такими, поддерживая такую функциональность, не соответствует реализаторам. Это означает, что некоторые реализации могут обеспечить безопасное одновременное выполнение областей OpenMP верхнего уровня, а некоторые - нет. Разработчики x86 обязуются поддерживать его, возможно, потому, что большинство из них также являются сторонниками других моделей исполнения (например, Intel с Cilk и TBB, GCC с С++ 11 и т.д.), А x86 обычно считается "экспериментальной" платформой ( другие поставщики обычно гораздо более консервативны).
OpenMP 4.0 также не идет дальше, чем ISO/IEC 14882: 1998 для функций С++, которые он использует (проект SC12 здесь), Стандарт теперь включает в себя такие вещи, как переносимость потоковой нити - это определенно не очень хорошо работает с другими парадигмами потоков, которые могут обеспечить свои собственные механизмы привязки, которые сталкиваются с проблемами OpenMP. Еще раз, язык OpenMP ориентирован на HPC (параллельные научные и инженерные приложения для данных и задач). Конструкции С++ 11 ориентированы на универсальные вычислительные приложения. Если вы хотите использовать одновременно С++ 11, то используйте только С++ 11, или если вам действительно нужно смешать его с OpenMP, то придерживайтесь подмножества языков С++ 98, если вы хотите оставаться портативным.
Меня особенно интересует ситуация, когда я сначала вызываю некоторый код с помощью OpenMP, а затем другой код с использованием С++ 11 concurrency в тех же структурах данных.
Нет никаких очевидных причин, по которым вы не хотите, но это зависит от вашего компилятора OpenMP и времени выполнения. Существуют бесплатные и коммерческие библиотеки, которые используют OpenMP для параллельного выполнения (например, MKL), но всегда есть предупреждения (хотя иногда они глубоко скрыты в руководствах пользователя) о возможной несовместимости с многопоточным кодом, которые предоставляют информацию о том, что и когда это возможно. Как всегда, это выходит за рамки стандарта OpenMP и, следовательно, YMMV.
Ответ 2
Мне действительно интересны высокопроизводительные вычисления, но OpenMP (в настоящее время) не обслуживает мои достаточно хорошо: он недостаточно гибкий (мой алгоритм не основан на петле)
Возможно, вы действительно ищете TBB?
Это обеспечивает поддержку циклов и задач на основе parallelism, а также множество параллельных структур данных в стандартном С++ и является как переносным, так и открытым исходным кодом.
(Полная оговорка: я работаю для Intel, которые активно участвуют в TBB, хотя я действительно не работаю на на TBB, но на OpenMP:-); Я, конечно, не говорю для Intel!).
Ответ 3
Как и Джим Кони, я тоже сотрудник Intel. Я согласен с ним в том, что Intel Threading Building Blocks (Intel TBB) может быть хорошим вариантом, поскольку он имеет уровень на уровне цикла parallelism, такой как OpenMP, а также другие параллельные алгоритмы, параллельные контейнеры и функции нижнего уровня. И TBB пытается идти в ногу с текущим стандартом С++.
И для уточнения для Уолтера, Intel TBB включает в себя алгоритм parallel_reduce, а также высокоуровневую поддержку для атомистики и мьютексов.
Руководство пользователя Intel® Threading Building Blocks можно найти в http://software.intel.com/sites/products/documentation/doclib/tbb_sa/help/tbb_userguide/title.htm. В Руководстве пользователя представлен обзор функций в библиотеке.
Ответ 4
OpenMP часто (я не знаю исключений), реализованного поверх Pthreads, поэтому вы можете рассуждать о некоторых вопросах взаимодействия, думая о том, как С++ 11 concurrency взаимодействует с кодом Pthread.
Я не знаю, является ли чрезмерная подписка из-за использования нескольких моделей потоковой передачи для вас проблемой, но это определенно проблема для OpenMP. Существует предложение для решения этой проблемы в OpenMP 5. До тех пор, как вы решаете это, определяется реализация. Это тяжелые молоты, но вы можете использовать OMP_WAIT_POLICY
(OpenMP 4.5+), KMP_BLOCKTIME
(Intel и LLVM) и GOMP_SPINCOUNT
(GCC), чтобы решить эту проблему. Я уверен, что в других реализациях есть что-то подобное.
Одна проблема, в которой интероперабельность представляет собой реальную проблему, - w.r.t. модель памяти, т.е. как ведут себя атомные операции. Это в настоящее время undefined, но вы все еще можете рассуждать об этом. Например, если вы используете атомарность С++ 11 с OpenMP parallelism, вы должны быть в порядке, но вы несете ответственность за правильное использование атома атома С++ 11 из потоков OpenMP.
Смешивание атома атома OpenMP и атома С++ 11 - плохая идея. Мы (рабочая группа по языковому комитету OpenMP, которому поручено смотреть на поддержку базового языка OpenMP 5), в настоящее время пытаются разобраться в этом. Лично я считаю, что атомистика С++ 11 лучше, чем атомы OpenMP во всех отношениях, поэтому моя рекомендация заключается в том, что вы используете С++ 11 (или C11 или __atomic
) для вашего атомизма и оставьте #pragma omp atomic
для программистов Fortran.
Ниже приведен пример пример кода, который использует атомы С++ 11 с потоками OpenMP. Он работает так, как он был разработан везде, где я его тестировал.
Полное раскрытие: Как и Джим и Майк, я работаю для Intel: -)
#if defined(__cplusplus) && (__cplusplus >= 201103L)
#include <iostream>
#include <iomanip>
#include <atomic>
#include <chrono>
#ifdef _OPENMP
# include <omp.h>
#else
# error No OpenMP support!
#endif
#ifdef SEQUENTIAL_CONSISTENCY
auto load_model = std::memory_order_seq_cst;
auto store_model = std::memory_order_seq_cst;
#else
auto load_model = std::memory_order_acquire;
auto store_model = std::memory_order_release;
#endif
int main(int argc, char * argv[])
{
int nt = omp_get_max_threads();
#if 1
if (nt != 2) omp_set_num_threads(2);
#else
if (nt < 2) omp_set_num_threads(2);
if (nt % 2 != 0) omp_set_num_threads(nt-1);
#endif
int iterations = (argc>1) ? atoi(argv[1]) : 1000000;
std::cout << "thread ping-pong benchmark\n";
std::cout << "num threads = " << omp_get_max_threads() << "\n";
std::cout << "iterations = " << iterations << "\n";
#ifdef SEQUENTIAL_CONSISTENCY
std::cout << "memory model = " << "seq_cst";
#else
std::cout << "memory model = " << "acq-rel";
#endif
std::cout << std::endl;
std::atomic<int> left_ready = {-1};
std::atomic<int> right_ready = {-1};
int left_payload = 0;
int right_payload = 0;
#pragma omp parallel
{
int me = omp_get_thread_num();
/// 0=left 1=right
bool parity = (me % 2 == 0);
int junk = 0;
/// START TIME
#pragma omp barrier
std::chrono::high_resolution_clock::time_point t0 = std::chrono::high_resolution_clock::now();
for (int i=0; i<iterations; ++i) {
if (parity) {
/// send to left
left_payload = i;
left_ready.store(i, store_model);
/// recv from right
while (i != right_ready.load(load_model));
//std::cout << i << ": left received " << right_payload << std::endl;
junk += right_payload;
} else {
/// recv from left
while (i != left_ready.load(load_model));
//std::cout << i << ": right received " << left_payload << std::endl;
junk += left_payload;
///send to right
right_payload = i;
right_ready.store(i, store_model);
}
}
/// STOP TIME
#pragma omp barrier
std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();
/// PRINT TIME
std::chrono::duration<double> dt = std::chrono::duration_cast<std::chrono::duration<double>>(t1-t0);
#pragma omp critical
{
std::cout << "total time elapsed = " << dt.count() << "\n";
std::cout << "time per iteration = " << dt.count()/iterations << "\n";
std::cout << junk << std::endl;
}
}
return 0;
}
#else // C++11
#error You need C++11 for this test!
#endif // C++11