我可以安全地使用C++11和OpenMP吗?

45

OpenMP标准仅支持C++ 98 (ISO/IEC 14882:1998)。这意味着没有标准支持在C++03甚至C++11下使用OpenMP。因此,任何使用C++ >98和OpenMP的程序都是超出标准范围的,这意味着即使它在某些条件下工作,也不太可能具有可移植性,而且绝对不能保证。

对于具有自身多线程支持的C++11来说,情况甚至更糟,这很可能会与某些实现中的OpenMP发生冲突。

那么,在使用C++03和C++11时,使用OpenMP安全吗?

一个程序中能否安全地同时使用C++11多线程和OpenMP,但不交错使用它们(即没有将OpenMP语句传递给使用C++11并发特性的任何代码,并且没有在OpenMP生成的线程中使用C++11并发)?

我特别关注的是首先调用使用OpenMP的一些代码,然后再调用使用C++11并发的其他代码,这些代码操作相同的数据结构。


31
是的,是的,是的,千次的肯定!这是一种可怕、可怕的预处理器黑科技,与语言整合非常不好,请消失吧!(免责声明:我已经在OpenMP之上编写了一个库,并写了一篇硕士论文;我至少表面上知道我在抱怨什么。) - Konrad Rudolph
1
是的,但不是你所写的原因;相反,我会问什么基础设施实际上支持这个标准?如果你想进行大规模并行计算,我会寻找可以在云计算平台上完成的东西(即使不是用C++);如果你必须建立自己的集群来使用OpenMP,那就不值得了。 - Michael Aaron Safyan
1
@MichaelAaronSafyan,我显然只是在谈论多线程,而不是分布式计算。如果你想要那个,你必须使用完全不同的东西。 - Walter
1
问题标题有点挑衅性。也许改成“如何安全地使用OpenMP?”并让人们自行决定是否放弃它。 - Peter Wood
1
我打算投票关闭此项,除非从标题中编辑掉“应该放弃”的部分。 - NPE
显示剩余7条评论
5个回答

27

Walter,我相信我不仅告诉了你在那个讨论中的事情,而且还向你提供了来自OpenMP语言委员会成员(即我的同事)的信息。

OpenMP被设计为轻量级数据并行附加到FORTRAN和C,后来扩展到C ++惯用法(例如,随机访问迭代器上的并行循环)以及通过引入显式任务来进行任务并行。它旨在尽可能跨越许多平台,并在三种语言中提供基本相同的功能。其执行模型相当简单-单线程应用程序在并行区域中分叉线程团队,运行一些计算任务,然后将团队合并回串行执行。来自并行团队的每个线程后来都可以分叉自己的团队,如果启用了嵌套并行性。

由于OpenMP的主要用途是在高性能计算中(毕竟,它的指令和执行模型是从高性能Fortran借鉴的),因此任何OpenMP实现的主要目标都是效率,而不是与其他线程范例的互操作性。在某些平台上,只有在OpenMP运行时控制进程线程时才能实现有效的实现。此外,对于OpenMP的某些方面,可能与其他线程构造物不兼容,例如当分叉两个或多个并发并行区域时由OMP_THREAD_LIMIT设置的线程数限制。

由于OpenMP标准本身并没有严格禁止使用其他线程范例,但也没有标准化与这种范例的互操作性,因此支持这种功能取决于实现者。这意味着一些实现可能提供顶级OpenMP区域的安全并发执行,而一些实现可能不会。x86实现者承诺支持它,可能是因为他们中的大多数人也是其他执行模型(例如Intel与Cilk和TBB,GCC与C ++11等)的支持者,而x86通常被认为是“实验性”平台(其他供应商通常要保守得多)。

OpenMP 4.0并没有超出ISO/IEC 14882:1998所使用的C++特性(SC12草案可以在这里找到)。现在标准包括一些诸如可移植线程亲和力之类的东西-这显然与其他线程范例不兼容,因为它们可能提供自己的绑定机制,这些机制与OpenMP的机制冲突。再一次地,OpenMP语言针对的是HPC(数据和任务并行科学和工程应用)。C++11结构则针对通用计算应用。如果你想要花哨的C++11并发功能,那就只用C++11,或者如果你真的需要将其与OpenMP混合使用,那么如果你想保持可移植性,就要坚持使用C++98语言特性的子集。

我特别关心的是,我首先调用使用OpenMP的代码,然后再调用使用C++11并发的其他代码来处理相同的数据结构。

你想要的情况没有明显的原因不能实现,但这取决于你的OpenMP编译器和运行时。有一些免费和商业库使用OpenMP进行并行执行(例如MKL),但总会有警告(尽管有时隐藏得很深在用户手册中)可能与多线程代码不兼容,提供什么和何时是可能的信息。与往常一样,这超出了OpenMP标准的范围,因此你的经验可能会有所不同。


只是想让您的评论成为答案;)。我实际上对高性能计算很感兴趣,但目前的OpenMP并不能完全满足我的需求:它不够灵活(我的算法不是基于循环的)。 - Walter

8

我实际上对高性能计算很感兴趣,但 OpenMP (目前)不能完全满足我的需求:它不够灵活(我的算法不是基于循环的)。

也许您真正需要的是 TBB ?它为基于循环和任务的并行性提供支持,以及各种并行数据结构,在标准 C++ 中,而且具有可移植性和开源性。

(全面声明:我在 Intel 工作,他们与 TBB 密切相关,尽管我不是真正工作在 TBB 上 - 我确实在 OpenMP 上工作 :-);我当然不代表 Intel!)


谢谢你的回答。我一定会在有时间的时候研究TBB。它支持什么样的同步技术?我对类似于MPI的Reduce的东西很感兴趣,即在几个运行线程之间进行归约。这可以做到吗? - Walter

5

我和Jim Cownie一样,也是Intel公司的员工。我赞同他的观点,认为Intel Threading Building Blocks (Intel TBB)可能是一个不错的选择,因为它具有类似于OpenMP的循环级别并行性,还提供其他并行算法、并发容器和更低级别的特性。TBB还试图跟上当前的C++标准。

为了澄清Walter的疑惑,Intel TBB包括parallel_reduce算法以及对原子操作和互斥锁的高级支持。

您可以在http://software.intel.com/sites/products/documentation/doclib/tbb_sa/help/tbb_userguide/title.htm找到Intel® Threading Building Block的用户指南。用户指南概述了库中的功能。


我尝试过Intel TBB。由于某些不明原因,整个代码变得非常缓慢且占用内存,总是抛出bad_alloc异常。而OpenMP版本则只需半分钟即可运行。 - user
2
@user 自从近3年前询问这个问题以来,我已经用tbb取得了出色的经验,并完全放弃了OpenMP。这有两个原因。首先,它使用模糊的# pragma方式必须在标准外运行,因此不可移植。其次,我的基于tbb的代码运行更快,而且tbb为多线程算法提供了更多的灵活性(尽管最近的OpenMP版本也做到这一点)。 - Walter

4

我知道没有例外,OpenMP通常是在Pthreads的基础上实现的,因此您可以通过考虑C++11并发如何与Pthread代码互操作来思考一些互操作性问题。

我不知道使用多个线程模型是否会导致过度订阅的问题困扰您,但这绝对是OpenMP面临的问题。提案 在OpenMP 5中解决了这个问题。在那之前,如何解决这个问题是由实现定义的。它们是重量级的工具,但您可以使用OMP_WAIT_POLICY(OpenMP 4.5+)、KMP_BLOCKTIME(Intel和LLVM)和GOMP_SPINCOUNT(GCC)来解决这个问题。我相信其他实现也有类似的东西。

一个真正需要互操作性的问题是内存模型,即原子操作的行为。这目前没有定义,但你仍然可以理解它。例如,如果您在OpenMP并行性中使用C++11原子,则应该没问题,但是您负责从OpenMP线程正确使用C++11原子。
混合使用OpenMP原子和C++11原子是一个不好的想法。我们(OpenMP语言委员会工作组负责查看OpenMP 5基础语言支持)目前正在努力解决这个问题。个人认为,C++11原子在各个方面都比OpenMP原子更好,因此我的建议是,您使用C++11(或C11或__atomic)进行原子操作,并将#pragma omp atomic留给Fortran程序员。
以下是一个使用C++11原子操作和OpenMP线程的示例代码。在我测试过的所有地方,它都按预期工作。
完整披露:像Jim和Mike一样,我也在英特尔工作 :-)
#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

1
感谢您详细的回答。自从回答这个问题以来,我已经完全转向使用tbb进行多线程编程,因为它对我的需求已经足够(而且比C++线程更完整,因为它带有任务调度器)。我特别担心缺乏将OpenMP与最近的C++混合使用的标准支持。这难道没有任何法律影响(对于执行此类混合的程序的属性)吗?[顺便说一句,使用“auto”而不是“std::chrono::high_resolution_clock::time_point”会使这段代码更易读] - Walter
我不知道是否有任何法律禁止OpenMP + C++11的使用,但我不是律师 :-) 从技术上讲,OpenMP不支持C++11,但如果它们的使用是正交的,它们应该可以一起工作,没有任何理由不能共同使用。 - Jeff Hammond

1

OpenMP 5.0现在定义了与C++11的交互。但通常使用C++11及更高版本的任何内容都可能导致"未指定的行为"

此OpenMP API规范将ISO/IEC 14882:2011称为C++11。虽然预计OpenMP规范的未来版本将解决以下功能,但目前使用它们可能会导致未指定的行为。

  • 对齐支持
  • 标准布局类型
  • 允许移动构造函数抛出异常
  • 定义移动特殊成员函数
  • 并发性
  • 数据依赖性排序:原子和内存模型
  • 标准库的添加
  • 线程本地存储
  • 带并发的动态初始化和销毁
  • C++11库

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接