C/C++多线程编程(2) – OpenMP并行计算初涉

OpenMP是一个方便多线程计算的一个标准。现代的编译器例如GCC就自带了OpenMP的实现,但是要开启OpenMP功能,需要在编译的时候加入 -fopenmp 参数。下面是一段Hello Xiaoxia的代码来展示OpenMP的用法。

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. int main(){  
  5.     #pragma omp parallel  
  6.     {  
  7.         cout << "Hello Xiaoxia!\n";  
  8.     }  
  9.     return 0;  
  10. }  

编译运行,

root@xiaoxia-pc:~/study/openmp# g++ mp.cpp -o mp -fopenmp && ./mp
Hello Xiaoxia!
Hello Xiaoxia!
Hello Xiaoxia!
Hello Xiaoxia!

和正常的Hello代码差不多,不同的地方是多了一句#pragma关于omp的一些声明,以及用花括号封装起来的语句(如果是单个语句,可以不要花括号)。
因为我的CPU是4线程的,所以OpenMP默认使用4线程来执行代码,运行结果看到4个Hello Xiaoxia!

对代码做点小改动,探究一下:

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. int main(){  
  5.     #pragma omp parallel  
  6.     {  
  7.         cout << "Hello Xiaoxia!\n";  
  8.         sleep(20);  
  9.     }  
  10.     return 0;  
  11. }  

编译运行:

root@xiaoxia-pc:~/study/openmp# g++ mp.cpp -o mp -fopenmp
root@xiaoxia-pc:~/study/openmp# ./mp &
[1] 15433
root@xiaoxia-pc:~/study/openmp# Hello Xiaoxia!
Hello Xiaoxia!
Hello Xiaoxia!
Hello Xiaoxia!
ls /proc/15433/task
15433 15434 15435 15436

由此验证OpenMP的确打开了4个线程……值得注意的是,一旦OpenMP在并行区域里启动了4个线程进行工作之后,并不会马上结束被系统回收,而是留作后面进行并行计算的时候继续使用!!!

使用omp_set_num_threads来改变线程个数!

  1. #include <stdio.h>  
  2. #include <omp.h>  
  3.   
  4. int main(){  
  5.     omp_set_num_threads(10);  
  6. #pragma omp parallel  
  7.     {  
  8.         printf("I'm %d!\n", omp_get_thread_num());  
  9.     }  
  10.     return 0;  
  11. }  

编译运行:

root@xiaoxia-pc:~/study/openmp# g++ mp.cpp -o mp -fopenmp
root@xiaoxia-pc:~/study/openmp# ./mp
I’m 0!
I’m 8!
I’m 1!
I’m 2!
I’m 3!
I’m 5!
I’m 6!
I’m 7!
I’m 4!
I’m 9!

OpenMP的多线程运算可以用在for循环里,可以选择不同的方式分配线程处理循环中的过程,最简单的用法如下:

  1. #include <stdio.h>  
  2. #include <omp.h>  
  3.   
  4. int main(){  
  5. #pragma omp parallel for  
  6.     for(int i=0; i<10; i++)  
  7.         printf("%d have %d\n", omp_get_thread_num(), i);  
  8.     return 0;  
  9. }  

编译运行,

root@xiaoxia-pc:~/study/openmp# g++ mp.cpp -o mp -fopenmp
root@xiaoxia-pc:~/study/openmp# ./mp
2 has 6
2 has 7
2 has 8
1 has 3
1 has 4
1 has 5
0 has 0
0 has 1
0 has 2
3 has 9

OK!OpenMP还有很多用法,我觉得用的比较通常的是,

#pragma omp single
只能由一个线程执行下一行代码,如果没有nowait参数,则其他线程运行到这里会等待。

#pragma omp master
跟single差不多,但是只能由主线程执行下一行代码,如果没有nowait参数,则其他线程运行到这里会等待。

#pragma omp for
指一个for循环里需要指派不同的线程执行不同的iteration(不知道中文怎么表达)。

#pragma omp critical
指下一行代码同时只能由一个线程执行(注意跟single的区别)。

OK!暂且了解这么多够了。先实践一下!

我在想除了计算PI,还有啥运算比较复杂的东西呢?像微积分、FFT那种太深奥的我又不是很会。最后想了一下还是用回文数吧,因为我高中用的手机号码是一个11位的回文数字,哈哈!但上大学之后用广州卡了 🙂

给出一个整数,如何快速判断是否为一个回文数字?没有想多远,留给各位读者思考了。我直接一个数字一个数字的比较,代码如下,

  1. /* Any faster methods ? */  
  2. bool is_reversible(unsigned int n)  
  3. {  
  4.     int digits[16], m=n, i=0, j;  
  5.     do{  
  6.         digits[i++] = m % 10;  
  7.         m /= 10;  
  8.     }while(m > 0);  
  9.     j = i >> 1;  
  10.     while(n>0 && i>j){  
  11.         if(digits[--i] != n % 10)  
  12.             return false;  
  13.         n /= 10;  
  14.     }  
  15.     return true;  
  16. }  

使用并行查找一个随机生成的整数数组data里面的回文数,

  1. void parallel_find()  
  2. {  
  3.     int rev_data = 0;  
  4. #pragma omp parallel shared(rev_data)  
  5.     {  
  6. #pragma omp for  
  7.         for(int i=0; i<NUM_DATA; i++)  
  8.             if(is_reversible(data[i]))  
  9. #pragma omp critical  
  10.                 rev_data ++;  
  11. #pragma omp master  
  12.         printf("rev: %u\n", rev_data);  
  13.     }  
  14. }  

先用parallel定义要并行运算的区域,程序运行到这里,便会创建一定数量的线程执行该区域的代码。
关键字for把i取值的不同区间分配给不同的线程来执行运算。
因为rev_data是多线程共享的变量,多线程写入操作的时候,使用了critical关键字来避免race condition(中文怎么翻译?)。
最后,使用主线程打印结果。

主程序如下:

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <omp.h>  
  4. #include <unistd.h>  
  5. #include <math.h>  
  6.   
  7. #define NUM_DATA 20000000  
  8. #define THREAD_COUNT 4  
  9.   
  10. unsigned int data[NUM_DATA];  
  11.   
  12. int main(){  
  13.     /* Give me more process priority */  
  14.     nice(-20);  
  15.     srand(0);  
  16. #ifdef OPENMP  
  17.     omp_set_num_threads(THREAD_COUNT);  
  18. #endif  
  19.       
  20.     /* Randomize test data */  
  21.     printf("Generating random numbers...\n");  
  22.     for(int i=0; i<NUM_DATA; i++)  
  23.         data[i] = rand();  
  24.       
  25.     for(int j=0; j<10; j++)  
  26.         parallel_find();  
  27.   
  28.     return 0;  
  29. }  

程序运行生成20000000个随机数,大约占用80MB内存,大约需要0.5秒。
然后使用for循环调用10次并行查找,目的是方便测试并行计算的性能。做实验一般都是多次操作,数据取平均值嘛!

系统:Kubuntu11.04
CPU:Intel(R) Core(TM) i3 CPU M 330 @ 2.13GHz (双核四线程)
内存:2GB

测试结果,

NUM_DATA = 20,000,000
THREAD_COUNT = 1

root@xiaoxia-pc:~/study/openmp# g++ mp.cpp -o mp
root@xiaoxia-pc:~/study/openmp# time ./mp
Generating random numbers…
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140

real 0m22.958s
user 0m22.897s
sys 0m0.040s

NUM_DATA = 20,000,000
THREAD_COUNT = 2

root@xiaoxia-pc:~/study/openmp# g++ mp.cpp -o mp -fopenmp -DOPENMP
root@xiaoxia-pc:~/study/openmp# time ./mp
Generating random numbers…
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140

real 0m14.061s
user 0m27.542s
sys 0m0.044s

NUM_DATA = 20,000,000
THREAD_COUNT = 4

root@xiaoxia-pc:~/study/openmp# g++ mp.cpp -o mp -fopenmp -DOPENMP
root@xiaoxia-pc:~/study/openmp# time ./mp
Generating random numbers…
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140

real 0m12.997s
user 0m49.575s
sys 0m0.080s

NUM_DATA = 20,000,000
THREAD_COUNT = 8

root@xiaoxia-pc:~/study/openmp# g++ mp.cpp -o mp -fopenmp -DOPENMP
root@xiaoxia-pc:~/study/openmp# time ./mp
Generating random numbers…
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140

real 0m13.504s
user 0m49.383s
sys 0m0.100s

NUM_DATA = 20,000,000
THREAD_COUNT = 16

root@xiaoxia-pc:~/study/openmp# g++ mp.cpp -o mp -fopenmp -DOPENMP
root@xiaoxia-pc:~/study/openmp# time ./mp
Generating random numbers…
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140
rev: 1140

real 0m14.062s
user 0m53.675s
sys 0m0.128s

观察不使用OpenMP的单线程计算与使用OpenMP的不同线程数目的并行计算。

在启用OpenMP使用双线程后,消耗时间减少了将近一半。在四个线程的时候,CPU功耗最大,运算速度达到最快,消耗时间最小。

观察上图,有点奇怪,我在想,Linux内核对user time的计算,是以CPU为单位还是以CPU线程为单位???如果是以CPU线程为单位,为何在两个线程的时候,user time没有翻倍呢?

内核态CPU在增长是显然的,因为线程多了,在内核态下调度的时间就多了 🙂

最后,使用OpenMP重写一下之前的一个多线程QuickSort快排。
详见,

C/C++多线程编程介绍(1) – QuickSort

我一开始在QuickSort递归函数里使用omp sections,结果速度慢了好10多倍!!!
后来发现是因为每次到达sections区域的时候,程序都会等待所有线程就绪,才会执行区域内的代码,这样,多线程不比单线程慢才怪!

下面介绍task关键字,在非递归或者递归函数里都很有用,因为它完美支持嵌套使用。我的理解是,task是用来调度空闲的线程来处理下一行代码的,如果当前没有空闲的线程,会等到其他线程空闲的时候再处理。

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <omp.h>  
  4. #include <unistd.h>  
  5.   
  6. #define MAX_DATA 20000000  
  7. #define THREAD_COUNT 16  
  8.   
  9. unsigned int data[MAX_DATA];  
  10.   
  11. void QuickSort(int begin, int end)  
  12. {  
  13.     int middle, i=begin, j=end;  
  14.     middle = data[(begin + end) >> 1];    
  15.       
  16.     /* Iterating */  
  17.     do{  
  18.         while(data[i] < middle) i++;  
  19.         while(data[j] > middle) j--;  
  20.         if(i <= j){  
  21.             register unsigned int t = data[i];  
  22.             data[i++] = data[j];  
  23.             data[j--] = t;  
  24.         }  
  25.     }while(i < j);  
  26.       
  27.     /* Divide & Conquer */  
  28.     if(begin < j)  
  29. #pragma omp task  
  30.         QuickSort(begin, j);  
  31.     if(i < end)  
  32. #pragma omp task  
  33.         QuickSort(i, end);  
  34. }  
  35.   
  36. int main(){  
  37.     nice(-20);  
  38.     omp_set_num_threads(THREAD_COUNT);  
  39.       
  40.     /* Randomize test data */  
  41.     printf("Generating random numbers...\n");  
  42.     for(int i=0; i<MAX_DATA; i++)  
  43.         data[i] = rand();  
  44.       
  45.     printf("Quick sorting\n");  
  46. #pragma omp parallel   
  47.     {  
  48. #pragma omp single nowait  
  49.         QuickSort(0, MAX_DATA-1);  
  50.     }  
  51.     return 0;  
  52. }  

测试数据,

MAX_DATA = 20,000,000
THREAD_COUNT = 1, 2, 4, 8, 16

直接上图表,

这个结果让我对OpenMP还是有点失望了,因为我发现无法达到我之前用pthread进行多线程运算的水平。可以到这里看看!

也许是openmp封装了不少东西吧!!!

C/C++多线程编程(2) – OpenMP并行计算初涉》有18个想法

  1. Caboo

    突然感觉自己看代码是那么的无力,我到底是什么了。
    昏昏欲睡,又梦醒时分,我想起我曾看过这篇文章,似乎还是小虾大神写的。
    我又想起,我今天曾去过医院。
    小虾大神,请原谅我没有仔细的看你这篇文章,因为我实在是太累了。
    我担心自己会在一瞬间死去,生命是如此的脆弱,我又能如何。

    回复
    1. Xiaoxia 文章作者

      我的博文结构基本上都这样,文章里有代码,代码里有文章。当然似曾相识啦!
      生命是如此顽强!

      回复
  2. bob

    OpenMP还有一些细节选项可以调一调的。不过总体来说局限比较大,而且基本只能在一台机器上用,比起开线程也就编程方便点而已。并行计算库还是如MPI这样的意义要大一点。

    回复

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据