openmp gcc

    技术2024-06-05  85

    OpenMP框架是使用C,C ++和Fortran进行并发编程的强大方法。 GNU编译器集合(GCC)4.2版支持OpenMP 2.5标准,而GCC 4.4支持最新的OpenMP 3标准。 其他编译器,包括Microsoft®Visual Studio,也支持OpenMP。 在本文中,您可以学习使用OpenMP编译器编译指示,查找对OpenMP提供的某些应用程序编程接口(API)的支持,以及使用某些并行算法测试OpenMP。 本文使用GCC 4.2作为首选编译器。

    入门

    OpenMP的一个很棒的功能是,除了标准的GCC安装之外,您不需要任何其他东西。 使用OpenMP的程序必须使用-fopenmp选项进行编译。

    您的第一个OpenMP程序

    让我们从一个简单的Hello,World开始! 打印应用程序,其中包含其他实用程序。 清单1显示了代码。

    清单1.带OpenMP的Hello World
    #include <iostream> int main() { #pragma omp parallel { std::cout << "Hello World!\n"; } }

    当使用g ++编译并运行清单1中的代码时,一个Hello,World! 应该显示在控制台中。 现在,使用-fopenmp选项重新编译代码。 清单2显示了输出。

    清单2.使用-fopenmp命令编译并运行代码
    tintin$ g++ test1.cpp -fopenmp tintin$ ./a.out Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!

    那么,发生了什么事? 仅当您指定-fopenmp编译器选项时, #pragma omp parallel的魔术才-fopenmp 。 在内部,在编译过程中,GCC会根据硬件和操作系统配置生成代码,以在运行时创建尽可能多的最佳线程,每个创建的线程的启动例程都是紧随编译指示之后的块中的代码。 这种行为是隐式并行化,而OpenMP的核心是一组强大的编译指示,使您不必执行大量的样板代码。 (为了进行比较,请检查一下您刚才所做的可移植操作系统接口(POSIX)线程[pthreads]实现是什么样的。)因为我使用的计算机运行的是具有四个物理核心的Intel®Core i7处理器每个物理核心有两个逻辑核心,清单2的输出似乎很合理(8个线程= 8个逻辑核心)。

    现在,让我们进入有关并行编译指示的更多详细信息。

    OpenMP并行的乐趣

    使用pragma的num_threads参数来控制线程数非常容易。 这又是清单1中的代码,其中指定的可用线程数为5(如清单3所示)。

    清单3.用num_threads控制线程数
    #include <iostream> int main() { #pragma omp parallel num_threads(5) { std::cout << "Hello World!\n"; } }

    代替num_threads方法,这是一种更改运行代码的线程数的替代方法。 这也将我们带到您将要使用的第一个OpenMP API: omp_set_num_threads 。 您可以在omp.h标头文件中定义此函数。 无需链接其他库即可使清单4中的代码正常工作-只需-fopenmp 。

    清单4.使用omp_set_num_threads来微调线程的创建
    #include <omp.h> #include <iostream> int main() { omp_set_num_threads(5); #pragma omp parallel { std::cout << "Hello World!\n"; } }

    最后,OpenMP还使用外部环境变量来控制其行为。 您可以调整清单2中的代码以仅打印Hello World! 通过将OMP_NUM_THREADS变量设置为6六次。 清单5显示了执行情况。

    清单5.使用环境变量来调整OpenMP行为
    tintin$ export OMP_NUM_THREADS=6 tintin$ ./a.out Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!

    您已经发现了OpenMP的所有三个方面:编译器编译指示,运行时API和环境变量。 如果同时使用环境变量和运行时API会发生什么? 运行时API的优先级更高。

    一个实际的例子

    OpenMP使用隐式并行化技术,您可以使用编译指示,显式函数和环境变量来指示编译器。 让我们看一个示例,其中OpenMP可以提供真正的帮助。 考虑清单6中的代码。

    清单6. for循环中的顺序处理
    int main( ) { int a[1000000], b[1000000]; // ... some initialization code for populating arrays a and b; int c[1000000]; for (int i = 0; i < 1000000; ++i) c[i] = a[i] * b[i] + a[i-1] * b[i+1]; // ... now do some processing with array c }

    显然,您可以将for循环拆分并运行到多个内核中,计算任何c[k]都不依赖c数组的其他元素。 清单7显示了OpenMP如何帮助您做到这一点。

    清单7. for循环中的并行处理,带有parallel for pragma
    int main( ) { int a[1000000], b[1000000]; // ... some initialization code for populating arrays a and b; int c[1000000]; #pragma omp parallel for for (int i = 0; i < 1000000; ++i) c[i] = a[i] * b[i] + a[i-1] * b[i+1]; // ... now do some processing with array c }

    parallel for pragma有助于将for循环工作负载分散到多个线程中,每个线程都可能在不同的内核上运行,从而显着减少了总计算时间。 清单8证明了这一点。

    清单8.了解omp_get_wtime
    #include <omp.h> #include <math.h> #include <time.h> #include <iostream> int main(int argc, char *argv[]) { int i, nthreads; clock_t clock_timer; double wall_timer; double c[1000000]; for (nthreads = 1; nthreads <=8; ++nthreads) { clock_timer = clock(); wall_timer = omp_get_wtime(); #pragma omp parallel for private(i) num_threads(nthreads) for (i = 0; i < 1000000; i++) c[i] = sqrt(i * 4 + i * 2 + i); std::cout << "threads: " << nthreads << " time on clock(): " << (double) (clock() - clock_timer) / CLOCKS_PER_SEC << " time on wall: " << omp_get_wtime() - wall_timer << "\n"; } }

    在清单8中 ,通过不断增加线程数来对运行内部for循环所需的时间进行基准测试。 omp_get_wtime API从任意但一致的点返回经过的墙壁时间(以秒为单位)。 因此, omp_get_wtime() - wall_timer返回观察到的墙时间以运行for循环。 clock()系统调用用于估计整个程序的处理器使用时间-也就是说,在报告最终数字之前,应汇总各个线程特定的处理器使用时间。 在我的Intel Core i7计算机上, 清单9显示了报告的内容。

    清单9.运行内部for循环的统计信息
    threads: 1 time on clock(): 0.015229 time on wall: 0.0152249 threads: 2 time on clock(): 0.014221 time on wall: 0.00618792 threads: 3 time on clock(): 0.014541 time on wall: 0.00444412 threads: 4 time on clock(): 0.014666 time on wall: 0.00440478 threads: 5 time on clock(): 0.01594 time on wall: 0.00359988 threads: 6 time on clock(): 0.015069 time on wall: 0.00303698 threads: 7 time on clock(): 0.016365 time on wall: 0.00258303 threads: 8 time on clock(): 0.01678 time on wall: 0.00237703

    尽管在执行过程中处理器时间几乎是相同的(除了创建线程和上下文切换的一些额外时间,它们应该是相同的),但是挂墙时间很有趣,并且随着线程数量的增加而逐渐减少,这意味着内核正在并行处理数据。 关于编译指示语法的最后说明: #pragma parallel for private(i)表示将循环变量i视为线程本地存储,每个线程都有该变量的副本。 线程局部变量未初始化。

    OpenMP的关键部分

    您并不是完全想让OpenMP弄清楚如何自动处理关键部分,是吗? 当然,您不必显式创建互斥(mutex),但是您仍然需要指定关键部分。 语法如下:

    #pragma omp critical (optional section name) { // no 2 threads can execute this code block concurrently }

    紧跟pragma omp critical的代码只能在给定时间由单个线程运行。 另外, optional section name是全局标识符,并且没有两个线程可以使用相同的全局标识符名称同时运行关键节。 考虑清单10中的代码。

    清单10.具有相同名称的多个关键部分
    #pragma omp critical (section1) { myhashtable.insert("key1", "value1"); } // ... other code follows #pragma omp critical (section1) { myhashtable.insert("key2", "value2"); }

    根据此代码,您可以放心地假设两个哈希表插入将永远不会同时发生,因为关键节名称是相同的。 这与您习惯使用pthread处理关键部分的方式略有不同,pthread的主要特征是使用(或滥用)锁。

    使用OpenMP锁定和互斥

    有趣的是,OpenMP带有自己的互斥体版本(因此毕竟不是所有编译指示):欢迎使用omp_lock_t ,它被定义为omp.h头文件的一部分。 常用的pthread式互斥操作适用,即使API名称相似。 您需要了解五个API:

    omp_init_lock :此API必须是访问omp_lock_t的第一个API,并且用于初始化。 请注意,在初始化之后,立即将锁视为未设置状态。 omp_destroy_lock :此API破坏了锁。 调用此API时,锁必须处于未设置状态,这意味着您无法调用omp_set_lock ,然后进行调用以销毁该锁。 omp_set_lock :此API设置omp_lock_t即获取互斥量。 如果线程无法设置锁定,则它将继续等待直到能够锁定为止。 omp_test_lock :如果锁定可用,则此API尝试锁定,如果成功则返回1 ,否则返回0 。 这是一个非阻塞API,也就是说,此函数不会使线程等待设置锁。 omp_unset_lock :此API释放锁定。

    清单11显示了一个传统的单线程队列的简单实现,该队列扩展为使用OpenMP锁处理多线程。 请注意,这并不是在每种情况下都应该做的正确的事,因此,此示例主要是为了便于说明。

    清单11.使用OpenMP扩展单线程队列
    #include <openmp.h> #include "myqueue.h" class omp_q : public myqueue<int> { public: typedef myqueue<int> base; omp_q( ) { omp_init_lock(&lock); } ~omp_q() { omp_destroy_lock(&lock); } bool push(const int& value) { omp_set_lock(&lock); bool result = this->base::push(value); omp_unset_lock(&lock); return result; } bool trypush(const int& value) { bool result = omp_test_lock(&lock); if (result) { result = result && this->base::push(value); omp_unset_lock(&lock); } return result; } // likewise for pop private: omp_lock_t lock; };

    嵌套锁

    OpenMP提供的其他类型的锁是omp_nest_lock_t锁变量。 这些类似于omp_lock_t ,其附加好处是这些锁可以被已经持有该锁的线程多次锁定。 每当保持线程使用omp_set_nest_lock重新获取嵌套锁时,都会增加一个内部计数器。 当一个或多个对omp_unset_nest_lock调用最终将内部锁计数器重置为0时,该锁便从保持线程中释放出来。 以下是用于omp_nest_lock_t的API:

    omp_init_nest_lock(omp_nest_lock_t* ) :此API将内部嵌套计数初始化为0 。 omp_destroy_nest_lock(omp_nest_lock_t* ) :此API销毁了锁。 在具有非零内部嵌套计数的锁上调用此API会导致未定义的行为。 omp_set_nest_lock(omp_nest_lock_t* ) :此API与omp_set_lock相似,不同之处在于线程在持有锁的同时可以多次调用此函数。 omp_test_nest_lock(omp_nest_lock_t* ) :此API是omp_set_nest_lock的非阻塞版本。 omp_unset_nest_lock(omp_nest_lock_t* ) :当内部计数器为0时,此API释放锁定。 否则,每次调用此方法都会使计数器递减。

    对任务执行的细粒度控制

    您已经看到所有线程pragma omp parallel运行pragma omp parallel的代码块。 可以进一步对该块中的代码进行分类,以供选择线程执行。 考虑清单12中的代码。

    清单12.学习使用并行部分的用法
    int main( ) { #pragma omp parallel { cout << "All threads run this\n"; #pragma omp sections { #pragma omp section { cout << "This executes in parallel\n"; } #pragma omp section { cout << "Sequential statement 1\n"; cout << "This always executes after statement 1\n"; } #pragma omp section { cout << "This also executes in parallel\n"; } } } }

    由所有线程并行运行在pragma omp sections之前但在pragma omp parallel之后的代码。 该成功的块pragma omp sections被进一步分为使用个人小节pragma omp section 。 每个pragma omp section块都可以由单个线程执行。 但是,section块中的各个语句始终按顺序运行。 清单13显示了清单12中的代码输出。

    清单13.运行清单12中的代码的输出
    tintin$ ./a.out All threads run this All threads run this All threads run this All threads run this All threads run this All threads run this All threads run this All threads run this This executes in parallel Sequential statement 1 This also executes in parallel This always executes after statement 1

    在清单13中,您再次拥有最初创建的八个线程。 在这8个线程中, pragma omp sections块中只有3个线程有足够的工作量。 在第二部分中,您指定运行打印语句的顺序。 这就是sections实用性背后的重点。 如果需要,您将能够指定代码块的顺序。

    结合并行循环了解firstprivate和lastprivate指令

    之前,您看到了使用private声明线程本地存储的过程。 那么,应该如何初始化线程局部变量? 也许在执行操作之前将它们与主线程中变量的值同步? 这是firstprivate指令派上用场的地方。

    第一私人指令

    使用firstprivate(variable) ,您可以将线程中的变量初始化为它在主变量中具有的任何值。 考虑清单14中的代码。

    清单14.使用与主线程不同步的线程局部变量
    #include <stdio.h> #include <omp.h> int main() { int idx = 100; #pragma omp parallel private(idx) { printf("In thread %d idx = %d\n", omp_get_thread_num(), idx); } }

    这是我得到的输出。 您的结果可能会有所不同。

    In thread 1 idx = 1 In thread 5 idx = 1 In thread 6 idx = 1 In thread 0 idx = 0 In thread 4 idx = 1 In thread 7 idx = 1 In thread 2 idx = 1 In thread 3 idx = 1

    清单15显示了带有firstprivate指令的代码。 如预期的那样,输出将在所有线程中打印初始化为100 idx 。

    清单15.使用firstprivate指令初始化线程局部变量
    #include <stdio.h> #include <omp.h> int main() { int idx = 100; #pragma omp parallel firstprivate(idx) { printf("In thread %d idx = %d\n", omp_get_thread_num(), idx); } }

    另外,请注意,您已使用omp_get_thread_num( )方法访问线程的ID。 这是从线程ID不同的是,Linux®的top指挥表演,这个方案只是为OpenMP的跟踪线程数的方式。 如果您打算将其与C++代码一起使用,则firstprivate注意firstprivate指令: firstprivate指令使用的变量是一个复制构造函数,可以从主线程的变量中初始化自身,因此,拥有一个对您的类私有的复制构造函数将总是会导致坏事。 现在让我们继续执行lastprivate指令,该指令在很多方面都是硬币的另一面。

    lastprivate指令

    现在,您打算将主线程的数据与最后一次运行循环计数生成的任何数据进行同步,而不是使用主线程的数据初始化线程局部变量。 清单16中的代码运行并行的for循环。

    清单16.在不与主线程进行数据同步的情况下使用并行for循环
    #include <stdio.h> #include <omp.h> int main() { int idx = 100; int main_var = 2120; #pragma omp parallel for private(idx) for (idx = 0; idx < 12; ++idx) { main_var = idx * idx; printf("In thread %d idx = %d main_var = %d\n", omp_get_thread_num(), idx, main_var); } printf("Back in main thread with main_var = %d\n", main_var); }

    在具有八个内核的开发计算机上,OpenMP最终为parallel for块创建了六个线程。 每个线程依次说明循环的两次迭代。 main_var的最终值取决于运行的最后一个线程,因此取决于该线程中的idx的值。 换句话说,价值main_var不取决于最后值idx但价值idx在哪个线程跑了过去。 清单17中的代码说明了这一点。

    清单17. main_var的值取决于最后运行的线程
    In thread 4 idx = 8 main_var = 64 In thread 2 idx = 4 main_var = 16 In thread 5 idx = 10 main_var = 100 In thread 3 idx = 6 main_var = 36 In thread 0 idx = 0 main_var = 0 In thread 1 idx = 2 main_var = 4 In thread 4 idx = 9 main_var = 81 In thread 2 idx = 5 main_var = 25 In thread 5 idx = 11 main_var = 121 In thread 3 idx = 7 main_var = 49 In thread 0 idx = 1 main_var = 1 In thread 1 idx = 3 main_var = 9 Back in main thread with main_var = 9

    几次运行清单17中的代码,可以确信主线程中main_var的值始终取决于上次运行线程中idx的值。 现在,如果要在循环中将主线程的值与idx的最终值同步怎么办? 这是lastprivate伪指令进入的地方,如清单18所示 。 与清单17中的代码类似,几次运行清单18中的代码,以使自己确信主线程中main_var的最终值为121 ( idx是最终循环计数器的值)。

    清单18.使用lastprivate指令进行同步
    #include <stdio.h> #include <omp.h> int main() { int idx = 100; int main_var = 2120; #pragma omp parallel for private(idx) lastprivate(main_var) for (idx = 0; idx < 12; ++idx) { main_var = idx * idx; printf("In thread %d idx = %d main_var = %d\n", omp_get_thread_num(), idx, main_var); } printf("Back in main thread with main_var = %d\n", main_var); }

    清单19显示了清单18的输出。

    清单19.清单18中代码的输出(请注意, main_var always在主线程中main_var always等于121)
    In thread 3 idx = 6 main_var = 36 In thread 2 idx = 4 main_var = 16 In thread 1 idx = 2 main_var = 4 In thread 4 idx = 8 main_var = 64 In thread 5 idx = 10 main_var = 100 In thread 3 idx = 7 main_var = 49 In thread 0 idx = 0 main_var = 0 In thread 2 idx = 5 main_var = 25 In thread 1 idx = 3 main_var = 9 In thread 4 idx = 9 main_var = 81 In thread 5 idx = 11 main_var = 121 In thread 0 idx = 1 main_var = 1 Back in main thread with main_var = 121

    最后一点:支持C++对象的lastprivate运算符要求相应的类具有可公开使用的operator=方法。

    与OpenMP合并排序

    让我们看一个真实的例子,了解OpenMP可以帮助您节省运行时间。 这不是merge sort的高度优化版本,但足以显示在代码中使用OpenMP的好处。 清单20显示了示例代码。

    清单20.使用OpenMP合并排序
    #include <omp.h> #include <vector> #include <iostream> using namespace std; vector<long> merge(const vector<long>& left, const vector<long>& right) { vector<long> result; unsigned left_it = 0, right_it = 0; while(left_it < left.size() && right_it < right.size()) { if(left[left_it] < right[right_it]) { result.push_back(left[left_it]); left_it++; } else { result.push_back(right[right_it]); right_it++; } } // Push the remaining data from both vectors onto the resultant while(left_it < left.size()) { result.push_back(left[left_it]); left_it++; } while(right_it < right.size()) { result.push_back(right[right_it]); right_it++; } return result; } vector<long> mergesort(vector<long>& vec, int threads) { // Termination condition: List is completely sorted if it // only contains a single element. if(vec.size() == 1) { return vec; } // Determine the location of the middle element in the vector std::vector<long>::iterator middle = vec.begin() + (vec.size() / 2); vector<long> left(vec.begin(), middle); vector<long> right(middle, vec.end()); // Perform a merge sort on the two smaller vectors if (threads > 1) { #pragma omp parallel sections { #pragma omp section { left = mergesort(left, threads/2); } #pragma omp section { right = mergesort(right, threads - threads/2); } } } else { left = mergesort(left, 1); right = mergesort(right, 1); } return merge(left, right); } int main() { vector<long> v(1000000); for (long i=0; i<1000000; ++i) v[i] = (i * i) % 1000000; v = mergesort(v, 1); for (long i=0; i<1000000; ++i) cout << v[i] << "\n"; }

    使用八个线程运行此merge sort我的运行时执行时间为2.1秒,而使用一个线程则为3.7秒。 这里唯一需要记住的是,您需要注意线程数。 我从八个线程入手:里程可能会因您的系统配置而异。 但是,如果没有显式的线程数,最终将创建数百个(甚至数千个)线程,并且系统性能下降的可能性非常高。 同样,前面讨论的杂注sections已与merge sort代码很好地结合使用。

    结论

    本文就是这样。 我们在这里介绍了一些基础知识:向您介绍了OpenMP并行编译指示; 了解了创建线程的不同方法; 使自己确信OpenMP提供了更好的时间性能,同步和细粒度的控制; 并通过带有merge sort的OpenMP的实际应用进行了包装。 但是,还有很多要研究的地方,最好的检查场所是OpenMP项目站点。 确保检查“ 相关主题”部分以获取更多详细信息。


    翻译自: https://www.ibm.com/developerworks/aix/library/au-aix-openmp-framework/index.html

    相关资源:gcc-5.1.0-tdm64-1-openmp.zip
    Processed: 0.013, SQL: 9