我可以安全地将 OpenMP 与 C++11 一起使用吗?

Posted

技术标签:

【中文标题】我可以安全地将 OpenMP 与 C++11 一起使用吗?【英文标题】:Can I safely use OpenMP with C++11? 【发布时间】:2012-11-30 00:50:57 【问题描述】:

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,但不将它们交错(即在传递给 C++11 并发特性的任何代码中都没有 OpenMP 语句,也没有 C++11 OpenMP 产生的线程中的并发性)?

我对首先使用 OpenMP 调用一些代码,然后在相同数据结构上使用 C++11 并发调用一些其他代码的情况特别感兴趣。

【问题讨论】:

是的,是的是的,一千次是的!与语言集成不佳的可怕,可怕的预处理器黑客,请死! (免责声明,我已经在 OpenMP 之上编写了一个库,并且我已经写了一篇关于此的硕士论文;我至少表面上知道我在咆哮什么。) 是的,但不是因为你写的原因;相反,我会问什么基础设施实际上支持这个标准?如果您正在寻找执行大规模并行计算,我会寻找可以在云计算平台上完成的事情(即使不是在 C++ 中);如果您必须构建自己的集群才能使用 OpenMP,那是不值得的。 @MichaelAaronSafyan 我显然只是在谈论多线程,而不是分布式计算。如果你想这样,你必须完全使用其他东西。 问题标题有点煽情。也许重命名为“我怎样才能安全地使用 OpenMP?”并让人们决定是否放弃它。 除非标题中的“应该放弃”位被删掉,否则我将投票结束这个不具建设性的内容。 【参考方案1】:

Walter,我相信我不仅在that other discussion 中告诉了您当前的情况,而且还直接从源头(即我的同事,他是 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 在其使用的 C++ 功能方面也没有比 ISO/IEC 14882:1998 更进一步(SC12 草案是here)。该标准现在包括诸如可移植线程亲和性之类的东西——这绝对不能与其他线程范例很好地配合,这些范例可能会提供与 OpenMP 冲突的自己的绑定机制。 OpenMP 语言再次针对 HPC(数据和任务并行的科学和工程应用程序)。 C++11 结构针对通用计算应用程序。如果你想要花哨的 C++11 并发东西,那么只使用 C++11,或者如果你真的需要将它与 OpenMP 混合,那么如果你想保持可移植性,那么坚持使用 C++98 语言特性子集。

我对首先使用 OpenMP 调用一些代码,然后在相同数据结构上使用 C++11 并发调用一些其他代码的情况特别感兴趣。

没有明显的理由说明您不希望实现什么,但这取决于您的 OpenMP 编译器和运行时。有使用 OpenMP 进行并行执行的免费和商业库(例如 MKL),但总是有警告(尽管有时隐藏在其用户手册中很深)可能与多线程代码不兼容,这些代码提供有关何时何地可能的信息。与往常一样,这超出了 OpenMP 标准和 YMMV 的范围。

【讨论】:

只是想让你的 cmets 成为一个答案;)。我实际上对高性能计算很感兴趣,但 OpenMP(目前)并不能很好地满足我的目的:它不够灵活(我的算法不是基于循环的)。【参考方案2】:

我实际上对高性能计算很感兴趣,但 OpenMP(目前)不适合我的 目的足够好:不够灵活(我的算法不是基于循环的)

也许您真的在寻找TBB? 它支持循环和基于任务的并行性,以及标准 C++ 中的各种并行数据结构,并且是可移植的和开源的。

(完全免责声明:我为与 TBB 密切相关的英特尔工作,虽然我实际上并没有在 TBB 上工作,而是在 OpenMP 上工作 :-);我当然不是代表英特尔!)。

【讨论】:

感谢您的回答。我一定会研究 TBB(一旦我有时间)。它支持什么样的同步技术?我会对类似于 MPI 的 Reduce 的东西感兴趣,即几个正在运行的线程之间的减少。这个可以吗?【参考方案3】:

和 Jim Cownie 一样,我也是一名英特尔员工。我同意他的观点,即英特尔线程构建模块 (英特尔 TBB) 可能是一个不错的选择,因为它具有像 OpenMP 这样的循环级并行性,还有其他并行算法、并发容器和较低级别的功能。 TBB 试图跟上当前的 C++ 标准。

为了向 Walter 澄清,英特尔 TBB 包括一个 parallel_reduce 算法以及对原子和互斥体的高级支持。

您可以在http://software.intel.com/sites/products/documentation/doclib/tbb_sa/help/tbb_userguide/title.htm 找到英特尔® 线程构建模块的用户指南。用户指南概述了库中的功能。

【讨论】:

我试过英特尔 TBB。由于一些不为人知的原因,整个代码变得如此缓慢且内存不足,它总是抛出 bad_alloc 异常。是否,OpneMP 版本在半分钟内运行。 @user 自从大约 3 年前提出这个问题以来,我对 tbb 有了很好的体验,并完全抛弃了 OpenMP。这有两个原因。首先是晦涩的#pragma 方式,它必然在标准之外运行,因此不可移植。其次,我的基于 tbb 的代码运行速度更快,并且 tbb 为多线程算法提供了更大的灵活性(不过,也许最近的 OpenMP 版本也可以实现)。【参考方案4】:

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

我不知道由于使用多线程模型而导致的超额订阅对您来说是否是个问题,但这对于 OpenMP 来说绝对是个问题。在 OpenMP 5 中有一个 proposal 来解决这个问题。在此之前,如何解决这个问题是实现定义的。它们是重锤,但您可以使用 OMP_WAIT_POLICY (OpenMP 4.5+)、KMP_BLOCKTIME (Intel 和 LLVM) 和 GOMP_SPINCOUNT (GCC) 来解决这个问题。我确信其他实现也有类似的东西。

真正关心互操作性的一个问题是 w.r.t.内存模型,即原子操作的行为方式。这是目前未定义的,但您仍然可以推理它。例如,如果您使用具有 OpenMP 并行性的 C++11 原子,则应该没问题,但您有责任在 OpenMP 线程中正确使用 C++11 原子。

混合使用 OpenMP 原子和 C++11 原子是一个坏主意。我们(负责研究 OpenMP 5 基础语言支持的 OpenMP 语言委员会工作组)目前正试图解决这个问题。就我个人而言,我认为 C++11 原子在各个方面都比 OpenMP 原子更好,所以我的建议是你使用 C++11(或 C11,或 __atomic)作为你的原子,而将 #pragma omp atomic 留给 Fortran 程序员.

下面是一个example code,它使用带有 OpenMP 线程的 C++11 原子。它在我测试过的所有地方都能正常工作。

全面披露:我和 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

【讨论】:

感谢您详细的回答。自从回答了这个问题后,我已经完全将 tbb 用于多线程,因为它足以满足我的需求(并且比 C++ 线程更完整,因为它带有任务调度程序)。我特别担心缺乏将 OpenMP 与最近的 C++ 混合的标准支持。这没有任何法律含义(对于进行这种混合的程序的属性)? [顺便说一句,使用auto 而不是std::chrono::high_resolution_clock::time_point 会使这段代码更具可读性] 我不知道有任何针对 OpenMP + C++11 的法律,但是 IANAL :-) 从技术上讲,OpenMP 不支持 C++11,但如果它们的用法是正交的,则没有他们没有任何理由不一起工作。【参考方案5】:

OpenMP 5.0 现在定义了与 C++11 的交互。但通常使用 C++11 及以后的任何内容“可能会导致未指定的行为”

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

对齐支持 标准布局类型 允许移动构造投掷 定义移动特殊成员函数 并发 数据依赖排序:原子和内存模型 对标准库的补充 线程本地存储 并发动态初始化和销毁​​ C++11 库

【讨论】:

以上是关于我可以安全地将 OpenMP 与 C++11 一起使用吗?的主要内容,如果未能解决你的问题,请参考以下文章

std::atomic 可以安全地与 OpenMP 一起使用吗

混合 C++11 原子和 OpenMP

无法使 OpenMP 与 CodeBlocks 和 GFortran 一起工作

将 OpenMP 与 clang 一起使用

将 OpenMP 与 Fortran 一起使用时出现内存错误,运行 FFTW

使用非线程安全随机数生成器在 C 中为 pi monte carlo 更正 OpenMP 编译指示