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

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

#include <iostream>
using namespace std;

int main(){
    #pragma omp parallel
    {
        cout << "Hello Xiaoxia!\n";
    }
    return 0;
}

编译运行,

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!

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

#include <iostream>
using namespace std;

int main(){
    #pragma omp parallel
    {
        cout << "Hello Xiaoxia!\n";
        sleep(20);
    }
    return 0;
}

编译运行:

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来改变线程个数!

#include <stdio.h>
#include <omp.h>

int main(){
    omp_set_num_threads(10);
#pragma omp parallel
    {
        printf("I'm %d!\n", omp_get_thread_num());
    }
    return 0;
}

编译运行:

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循环里,可以选择不同的方式分配线程处理循环中的过程,最简单的用法如下:

#include <stdio.h>
#include <omp.h>

int main(){
#pragma omp parallel for
    for(int i=0; i<10; i++)
        printf("%d have %d\n", omp_get_thread_num(), i);
    return 0;
}

编译运行,

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位的回文数字,哈哈!但上大学之后用广州卡了 🙂

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

/* Any faster methods ? */
bool is_reversible(unsigned int n)
{
    int digits[16], m=n, i=0, j;
    do{
        digits[i++] = m % 10;
        m /= 10;
    }while(m > 0);
    j = i >> 1;
    while(n>0 && i>j){
        if(digits[--i] != n % 10)
            return false;
        n /= 10;
    }
    return true;
}

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

void parallel_find()
{
    int rev_data = 0;
#pragma omp parallel shared(rev_data)
    {
#pragma omp for
        for(int i=0; i<NUM_DATA; i++)
            if(is_reversible(data[i]))
#pragma omp critical
                rev_data ++;
#pragma omp master
        printf("rev: %u\n", rev_data);
    }
}

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

主程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
#include <unistd.h>
#include <math.h>

#define NUM_DATA 20000000
#define THREAD_COUNT 4

unsigned int data[NUM_DATA];

int main(){
    /* Give me more process priority */
    nice(-20);
    srand(0);
#ifdef OPENMP
    omp_set_num_threads(THREAD_COUNT);
#endif
    
    /* Randomize test data */
    printf("Generating random numbers...\n");
    for(int i=0; i<NUM_DATA; i++)
        data[i] = rand();
    
    for(int j=0; j<10; j++)
        parallel_find();

    return 0;
}

程序运行生成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是用来调度空闲的线程来处理下一行代码的,如果当前没有空闲的线程,会等到其他线程空闲的时候再处理。

#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
#include <unistd.h>

#define MAX_DATA 20000000
#define THREAD_COUNT 16

unsigned int data[MAX_DATA];

void QuickSort(int begin, int end)
{
    int middle, i=begin, j=end;
    middle = data[(begin + end) >> 1];  
    
    /* Iterating */
    do{
        while(data[i] < middle) i++;
        while(data[j] > middle) j--;
        if(i <= j){
            register unsigned int t = data[i];
            data[i++] = data[j];
            data[j--] = t;
        }
    }while(i < j);
    
    /* Divide & Conquer */
    if(begin < j)
#pragma omp task
        QuickSort(begin, j);
    if(i < end)
#pragma omp task
        QuickSort(i, end);
}

int main(){
    nice(-20);
    omp_set_num_threads(THREAD_COUNT);
    
    /* Randomize test data */
    printf("Generating random numbers...\n");
    for(int i=0; i<MAX_DATA; i++)
        data[i] = rand();
    
    printf("Quick sorting\n");
#pragma omp parallel 
    {
#pragma omp single nowait
        QuickSort(0, MAX_DATA-1);
    }
    return 0;
}

测试数据,

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来减少垃圾评论。了解我们如何处理您的评论数据