OpenMP编程指南

转自:http://blog.csdn.net/drzhouweiming/archive/2008/01/10/2033276.aspx

10.1.1 private子句
private子句用于将一个或多个变量声明成线程私有的变量,变量声明成私有变量后,指定每个线程都有它自己的变量私有副本,其他线程无法访问私有副本。即使在并行区域外有同名的共享变量,共享变量在并行区域内不起任何作用,并且并行区域内不会操作到外面的共享变量。
private子句的用法格式如下:
private(list)
下面便是一个使用private子句的代码例子:
int k = 100;
#pragma omp parallel for private(k)
for ( k=0; k < 10; k++)
{
printf("k=%d/n", k);
}

printf("last k=%d/n", k);
上面程序执行后打印的结果如下:
k=6
k=7
k=8
k=9
k=0
k=1
k=2
k=3
k=4
k=5
last k=100
从打印结果可以看出,for循环前的变量k和循环区域内的变量k其实是两个不同的变量。
用private子句声明的私有变量的初始值在并行区域的入口处是未定义的,它并不会继承同名共享变量的值。
出现在reduction子句中的参数不能出现在private子句中。
10.1.2 firstprivate子句
private声明的私有变量不能继承同名变量的值,但实际情况中有时需要继承原有共享变量的值,OpenMP提供了firstprivate子句来实现这个功能。
先看一下以下的代码例子
int k = 100;
#pragma omp parallel for firstprivate(k)
for ( i=0; i < 4; i++)
{
k+=i;
printf("k=%d/n",k);
}

printf("last k=%d/n", k);

上面代码执行后打印结果如下:
k=100
k=101
k=103
k=102
last k=100

从打印结果可以看出,并行区域内的私有变量k继承了外面共享变量k的值100作为初始值,并且在退出并行区域后,共享变量k的值保持为100未变。
10.1.3 lastprivate子句
有时在并行区域内的私有变量的值经过计算后,在退出并行区域时,需要将它的值赋给同名的共享变量,前面的private和firstprivate子句在退出并行区域时都没有将私有变量的最后取值赋给对应的共享变量,lastprivate子句就是用来实现在退出并行区域时将私有变量的值赋给共享变量。
举个例子如下:
int k = 100;
#pragma omp parallel for firstprivate(k),lastprivate(k)
for ( i=0; i < 4; i++)
{
k+=i;
printf("k=%d/n",k);
}
printf("last k=%d/n", k);
上面代码执行后的打印结果如下:
k=100
k=101
k=103
k=102
last k=103
从打印结果可以看出,退出for循环的并行区域后,共享变量k的值变成了103,而不是保持原来的100不变。
由于在并行区域内是多个线程并行执行的,最后到底是将那个线程的最终计算结果赋给了对应的共享变量呢?OpenMP规范中指出,如果是循环迭代,那么是将最后一次循环迭代中的值赋给对应的共享变量;如果是section构造,那么是最后一个section语句中的值赋给对应的共享变量。注意这里说的最后一个section是指程序语法上的最后一个,而不是实际运行时的最后一个运行完的。
如果是类(class)类型的变量使用在lastprivate参数中,那么使用时有些限制,需要一个可访问的,明确的缺省构造函数,除非变量也被使用作为firstprivate子句的参数;还需要一个拷贝赋值操作符,并且这个拷贝赋值操作符对于不同对象的操作顺序是未指定的,依赖于编译器的定义。
10.1.4 threadprivate子句
threadprivate子句用来指定全局的对象被各个线程各自复制了一个私有的拷贝,即各个线程具有各自私有的全局对象。
用法如下:
#pragma omp threadprivate(list) new-line
下面用threadprivate命令来实现一个各个线程私有的计数器,各个线程使用同一个函数来实现自己的计数。计数器代码如下:
int counter = 0;
#pragma omp threadprivate(counter)
int increment_counter()
{
counter++;
return(counter);
}
如果对于静态变量也同样可以使用threadprivate声明成线程私有的,上面的counter变量如改成用static类型来实现时,代码如下:
int increment_counter2()
{
static int counter = 0;
#pragma omp threadprivate(counter)
counter++;
return(counter);
}


threadprivate和private的区别在于threadprivate声明的变量通常是全局范围内有效的,而private声明的变量只在它所属的并行构造中有效。
threadprivate的对应只能用于copyin,copyprivate,schedule,num_threads和if子句中,不能用于任何其他子句中。
用作threadprivate的变量的地址不能是常数。
对于C++的类(class)类型变量,用作threadprivate的参数时有些限制,当定义时带有外部初始化时,必须具有明确的拷贝构造函数。
对于windows系统,threadprivate不能用于动态装载(使用LoadLibrary装载)的DLL中,可以用于静态装载的DLL中,关于windows系统中的更多限制,请参阅MSDN中有关threadprivate子句的帮助材料。
有关threadprivate命令的更多限制方面的信息,详情请参阅OpenMP2.5规范。
10.1.5 shared子句
shared子句用来声明一个或多个变量是共享变量。
用法如下:
shared(list)
需要注意的是,在并行区域内使用共享变量时,如果存在写操作,必须对共享变量加以保护,否则不要轻易使用共享变量,尽量将共享变量的访问转化为私有变量的访问。
循环迭代变量在循环构造区域里是私有的。声明在循环构造区域内的自动变量都是私有的。
10.1.6 default子句
default子句用来允许用户控制并行区域中变量的共享属性。
用法如下:
default(shared | none)
使用shared时,缺省情况下,传入并行区域内的同名变量被当作共享变量来处理,不会产生线程私有副本,除非使用private等子句来指定某些变量为私有的才会产生副本。
如果使用none作为参数,那么线程中用到的变量必须显示指定是共享的还是私有的,除了那些由明确定义的除外。
10.1.7 reduction子句
reduction子句主要用来对一个或多个参数条目指定一个操作符,每个线程将创建参数条目的一个私有拷贝,在区域的结束处,将用私有拷贝的值通过指定的运行符运算,原始的参数条目被运算结果的值更新。

reduction子句用法如下:
reduction(operator:list)
下表列出了可以用于reduction子句的一些操作符以及对应私有拷贝变量缺省的初始值,私有拷贝变量的实际初始值依赖于redtucion变量的数据类型。
表10-4-1:reduction操作中各种操作符号对应拷贝变量的缺省初始值
Operator Initialization value
+ 0
* 1
- 0
& ~0
| 0
^ 0
&& 1
|| 0


例如一个整数求和的程序如下:
int i, sum = 100;

#pragma omp parallel for reduction(+: sum)
for ( i = 0; i < 1000; i++ )
{
sum += i;
}

printf( "sum = %ld/n", sum);

注意,如果在并行区域内不加锁保护就直接对共享变量进行写操作,存在数据竞争问题,会导致不可预测的异常结果。共享数据作为private、firstprivate、lastprivate、threadprivate、reduction子句的参数进入并行区域后,就变成线程私有了,不需要加锁保护了。
10.1.8 copyin子句
copyin子句用来将主线程中threadprivate变量的值拷贝到执行并行区域的各个线程的threadprivate变量中,便于线程可以访问主线程中的变量值,
用法如下:
copyin(list)

copyin中的参数必须被声明成threadprivate的,对于类类型的变量,必须带有明确的拷贝赋值操作符。
对于前面threadprivate中讲过的计数器函数,如果多个线程使用时,各个线程都需要对全局变量counter的副本进行初始化,可以使用copyin子句来实现,示例代码如下:
int main(int argc, char* argv[])
{
int iterator;
#pragma omp parallel sections copyin(counter)
{
#pragma omp section
{
int count1;
for ( iterator = 0; iterator < 100; iterator++ )
{
count1 = increment_counter();
}
printf("count1 = %ld/n", count1);
}
#pragma omp section
{
int count2;
for ( iterator = 0; iterator < 200; iterator++ )
{
count2 = increment_counter();
}
printf("count2 = %ld/n", count2);
}
}
printf("counter = %ld/n", counter);
}
打印结果如下:
count1 = 100
count2 = 200
counter = 0

从打印结果可以看出,两个线程都正确实现了各自的计数。
10.1.9 copyprivate子句
copyprivate子句提供了一种机制用一个私有变量将一个值从一个线程广播到执行同一并行区域的其他线程。
用法如下:
copyprivate(list)
copyprivate子句可以关联single构造,在single构造的barrier到达之前就完成了广播工作。copyprivate可以对private和threadprivate子句中的变量进行操作,但是当使用single构造时,copyprivate的变量不能用于private和firstprivate子句中。

下面便是一个使用copyprivate的代码例子:
int counter = 0;
#pragma omp threadprivate(counter)
int increment_counter()
{
counter++;
return(counter);
}
#pragma omp parallel
{
int count;
#pragma omp single copyprivate(counter)
{
counter = 50;
}
count = increment_counter();
printf("ThreadId: %ld, count = %ld/n", omp_get_thread_num(), count);
}
打印结果为:
ThreadId: 2, count = 51
ThreadId: 0, count = 51
ThreadId: 3, count = 51
ThreadId: 1, count = 51

如果没有使用copyprivate子句,那么打印结果为:
ThreadId: 2, count = 1
ThreadId: 1, count = 1
ThreadId: 0, count = 51
ThreadId: 3, count = 1

从打印结果可以看出,使用copyprivate子句后,single构造内给counter赋的值被广播到了其他线程里,但没有使用copyprivate子句时,只有一个线程获得了single构造内的赋值,其他线程没有获取single构造内的赋值。
参考文献:
Ananth Grama, Anshul Gupta,“并行计算导论”,张武等译,机械工业出版社,2005.01

Michael J. Quinn, “MPI与OpenMP并行程序设计”,陈文光等译,清华大学出版社,2004.10

Shameem Akhter等,“多核程序设计技术-通过软件多线程提升性能”,电子工业出版社,2007.03
OpenMP2.5规范 http://www.openmp.org/
OpenMP2.0规范 http://www.openmp.org/
MSDN帮助材料 ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.VisualStudio.v80.chs/dv_vclang/html/652414c5-78ed-4b7f-8283-1a9fe4c5e78d.htm
注:本文的写作主要参考OpenMP2.5规范等参考文献

/////////////////////////////////////////////////////////////////////////////////////////////////

转自:http://blog.csdn.net/drzhouweiming/archive/2007/10/26/1844762.aspx

OpenMP中的任务调度
OpenMP中,任务调度主要用于并行的for循环中,当循环中每次迭代的计算量不相等时,如果简单地给各个线程分配相同次数的迭代的话,会造成各个线程计算负载不均衡,这会使得有些线程先执行完,有些后执行完,造成某些CPU核空闲,影响程序性能。例如以下代码:
int i, j;
int a[100][100] = {0};
for ( i =0; i < 100; i++)
{
for( j = i; j < 100; j++ )
{
a[i][j] = i*j;
}
}
如果将最外层循环并行化的话,比如使用4个线程,如果给每个线程平均分配25次循环迭代计算的话,显然i=0和i=99的计算量相差了100倍,那么各个线程间可能出现较大的负载不平衡情况。为了解决这些问题,OpenMP中提供了几种对for循环并行化的任务调度方案。
在OpenMP中,对for循环并行化的任务调度使用schedule子句来实现,下面介绍schedule字句的用法。
1.1.1 Schedule子句用法
schedule子句的使用格式为:
schedule(type[,size])
schedule有两个参数:type和size,size参数是可选的。
1. type参数
表示调度类型,有四种调度类型如下:
· dynamic
· guided
· runtime
· static
这四种调度类型实际上只有static、dynamic、guided三种调度方式,runtime实际上是根据环境变量来选择前三种中的某中类型。
run-sched-var
2. size参数 (可选)
size参数表示循环迭代次数,size参数必须是整数。static、dynamic、guided三种调度方式都可以使用size参数,也可以不使用size参数。当type参数类型为runtime时,size参数是非法的(不需要使用,如果使用的话编译器会报错)。
1.1.2 静态调度(static)
当parallel for编译指导语句没有带schedule子句时,大部分系统中默认采用static调度方式,这种调度方式非常简单。假设有n次循环迭代,t个线程,那么给每个线程静态分配大约n/t次迭代计算。这里为什么说大约分配n/t次呢?因为n/t不一定是整数,因此实际分配的迭代次数可能存在差1的情况,如果指定了size参数的话,那么可能相差一个size。
静态调度时可以不使用size参数,也可以使用size参数。
3. 不使用size参数
不使用size参数时,分配给每个线程的是n/t次连续的迭代,不使用size参数的用法如下:
schedule(static)
例如以下代码:
#pragma omp parallel for schedule(static)
for(i = 0; i < 10; i++ )
{
printf("i=%d, thread_id=%d/n", i, omp_get_thread_num());
}

上面代码执行时打印的结果如下:
i=0, thread_id=0
i=1, thread_id=0
i=2, thread_id=0
i=3, thread_id=0
i=4, thread_id=0
i=5, thread_id=1
i=6, thread_id=1
i=7, thread_id=1
i=8, thread_id=1
i=9, thread_id=1
可以看出线程0得到了0~4次连续迭代,线程1得到5~9次连续迭代。注意由于多线程执行时序的随机性,每次执行时打印的结果顺序可能存在差别,后面的例子也一样。
4. 使用size参数
使用size参数时,分配给每个线程的size次连续的迭代计算,用法如下:
schedule(static, size)
例如以下代码:
#pragma omp parallel for schedule(static, 2)
for(i = 0; i < 10; i++ )
{
printf("i=%d, thread_id=%d/n", i, omp_get_thread_num());
}
执行时会打印以下结果:
i=0, thread_id=0
i=1, thread_id=0
i=4, thread_id=0
i=5, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=2, thread_id=1
i=3, thread_id=1
i=6, thread_id=1
i=7, thread_id=1
从打印结果可以看出,0、1次迭代分配给线程0,2、3次迭代分配给线程1,4、5次迭代分配给线程0,6、7次迭代分配给线程1,…。每个线程依次分配到2次连续的迭代计算。
1.1.3 动态调度(dynamic)
动态调度是动态地将迭代分配到各个线程,动态调度可以使用size参数也可以不使用size参数,不使用size参数时是将迭代逐个地分配到各个线程,使用size参数时,每次分配给线程的迭代次数为指定的size次。
下面为使用动态调度不带size参数的例子:
#pragma omp parallel for schedule(dynamic)
for(i = 0; i < 10; i++ )
{
printf("i=%d, thread_id=%d/n", i, omp_get_thread_num());
}
打印结果如下:
i=0, thread_id=0
i=1, thread_id=1
i=2, thread_id=0
i=3, thread_id=1
i=5, thread_id=1
i=6, thread_id=1
i=7, thread_id=1
i=8, thread_id=1
i=4, thread_id=0
i=9, thread_id=1

下面为动态调度使用size参数的例子:
#pragma omp parallel for schedule(dynamic, 2)
for(i = 0; i < 10; i++ )
{
printf("i=%d, thread_id=%d/n", i, omp_get_thread_num());
}
打印结果如下:
i=0, thread_id=0
i=1, thread_id=0
i=4, thread_id=0
i=2, thread_id=1
i=5, thread_id=0
i=3, thread_id=1
i=6, thread_id=0
i=8, thread_id=1
i=7, thread_id=0
i=9, thread_id=1
从打印结果可以看出第0、1,4、5,6、7次迭代被分配给了线程0,第2、3,8、9次迭代则分配给了线程1,每次分配的迭代次数为2。
1.1.4 guided调度(guided)
guided调度是一种采用指导性的启发式自调度方法。开始时每个线程会分配到较大的迭代块,之后分配到的迭代块会逐渐递减。迭代块的大小会按指数级下降到指定的size大小,如果没有指定size参数,那么迭代块大小最小会降到1。
例如以下代码:
#pragma omp parallel for schedule(guided,2)
for(i = 0; i < 10; i++ )
{
printf("i=%d, thread_id=%d/n", i, omp_get_thread_num());
}
打印结果如下:
i=0, thread_id=0
i=1, thread_id=0
i=2, thread_id=0
i=3, thread_id=0
i=4, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=5, thread_id=1
i=6, thread_id=1
i=7, thread_id=1
第0、1、2、3、4次迭代被分配给线程0,第5、6、7次迭代被分配给线程1,第8、9次迭代被分配给线程0,分配的迭代次数呈递减趋势,最后一次递减到2次。
1.1.5 runtime调度(rumtime)
runtime调度并不是和前面三种调度方式似的真实调度方式,它是在运行时根据环境变量OMP_SCHEDULE来确定调度类型,最终使用的调度类型仍然是上述三种调度方式中的某种。
例如在unix系统中,可以使用setenv命令来设置OMP_SCHEDULE环境变量:
setenv OMP_SCHEDULE “dynamic, 2”
上述命令设置调度类型为动态调度,动态调度的迭代次数为2。
在windows环境中,可以在”系统属性|高级|环境变量”对话框中进行设置环境变量。

//////////////////////////////////////////////////////////////////////////////////////////////////////////

http://blog.csdn.net/drzhouweiming/archive/2007/07/13/1689853.aspx

OpenMP创建线程中的锁及原子操作性能比较

在多核CPU中锁竞争到底会造成性能怎样的下降呢?相信这是许多人想了解的,因此特地写了一个测试程序来测试原子操作,windows CriticalSection, OpenMP的锁操作函数在多核CPU中的性能。

原子操作选用InterlockedIncrement来进行测试,
对每种锁和原子操作,都测试在单任务执行和多任务执行2000000次加锁解锁操作所消耗的时间。
测试的详细代码见后面。

测试机器环境: Intel 2.66G 双核CPU 机器一台

测试运行结果如下:
SingleThread, InterlockedIncrement 2,000,000: a = 2000000, time = 78
MultiThread, InterlockedIncrement 2,000,000: a = 2000000, time = 156
SingleThread, Critical_Section 2,000,000:a = 2000000, time = 172
MultiThread, Critical_Section, 2,000,000:a = 2000000, time = 3156
SingleThread,omp_lock 2,000,000:a = 2000000, time = 250
MultiThread,omp_lock 2,000,000:a = 2000000, time = 1063

在单任务运行情况下,所消耗的时间如下:
原子操作 78ms
Windows CriticalSection 172ms
OpenMP 的lock操作 250ms

因此从单任务情况来看,原子操作最快,Windows CriticalSection次之,OpenMP库带的锁最慢,但这几种操作的时间差距不是很大,用锁操作比原子操作慢了2~3倍左右。

在多个任务运行的情况下,所消耗的时间如下:

原子操作 156ms
Windows CriticalSection 3156ms
OpenMP 的lock操作 1063ms

在多任务运行情况下,情况发生了意想不到的变化,原子操作时间比单任务操作时慢了一倍,在两个CPU上运行比在单个CPU上运行还慢一倍,真是难以想象,估计是任务切换开销造成的。
Windows CriticalSection则更离谱了,居然花了3156ms,是单任务运行时的18倍多的时间,慢得简直无法想象。
OpenMP的lock操作比Windows CriticalSection稍微好一些,但也花了1063ms,是单任务时的7倍左右。

由此可以知道,在多核CPU的多任务环境中,原子操作是最快的,而OpenMP次之,Windows CriticalSection则最慢。

同时从这些锁在单任务和多任务下的性能差距可以看出,,多核CPU上的编程和以往的单核多任务编程会有很大的区别。
需要说明的是,本测试是一种极端情况下的测试,锁住的操作只是一个简单的加1操作,并且锁竞争次数达200万次之多,在实际情况中,一由于任务中还有很多不需要加锁的代码在运行,实际情况中的性能会比本测试的性能好很多。


测试代码如下:

// TestLock.cpp : OpenMP任务中的原子操作和锁性能测试程序。
//

#include
#include
#include
#include
#include

void TestAtomic()
{
clock_t t1,t2;
int i = 0;
volatile LONG a = 0;

t1 = clock();

for( i = 0; i < 2000000; i++ )
{
InterlockedIncrement( &a);
}

t2 = clock();
printf("SingleThread, InterlockedIncrement 2,000,000: a = %ld, time = %ld/n", a, t2-t1);

t1 = clock();

#pragma omp parallel for
for( i = 0; i < 2000000; i++ )
{
InterlockedIncrement( &a);
}

t2 = clock();
printf("MultiThread, InterlockedIncrement 2,000,000: a = %ld, time = %ld/n", a, t2-t1);
}

void TestOmpLock()
{
clock_t t1,t2;
int i;
int a = 0;
omp_lock_t mylock;

omp_init_lock(&mylock);

t1 = clock();

for( i = 0; i < 2000000; i++ )
{
omp_set_lock(&mylock);
a+=1;
omp_unset_lock(&mylock);
}
t2 = clock();

printf("SingleThread,omp_lock 2,000,000:a = %ld, time = %ld/n", a, t2-t1);

t1 = clock();

#pragma omp parallel for
for( i = 0; i < 2000000; i++ )
{
omp_set_lock(&mylock);
a+=1;
omp_unset_lock(&mylock);
}
t2 = clock();

printf("MultiThread,omp_lock 2,000,000:a = %ld, time = %ld/n", a, t2-t1);

omp_destroy_lock(&mylock);
}



void TestCriticalSection()
{
clock_t t1,t2;
int i;
int a = 0;
CRITICAL_SECTION cs;

InitializeCriticalSection(&cs);

t1 = clock();

for( i = 0; i < 2000000; i++ )
{
EnterCriticalSection(&cs);
a+=1;
LeaveCriticalSection(&cs);
}
t2 = clock();

printf("SingleThread, Critical_Section 2,000,000:a = %ld, time = %ld/n", a, t2-t1);

t1 = clock();

#pragma omp parallel for
for( i = 0; i < 2000000; i++ )
{
EnterCriticalSection(&cs);
a+=1;
LeaveCriticalSection(&cs);
}
t2 = clock();

printf("MultiThread, Critical_Section, 2,000,000:a = %ld, time = %ld/n", a, t2-t1);

DeleteCriticalSection(&cs);

}

int main(int argc, char* argv[])
{

TestAtomic();
TestCriticalSection();
TestOmpLock();

return 0;
}

////////////////////////////////////////////////////////////////////////////////

http://blog.csdn.net/drzhouweiming/archive/2008/05/23/2472454.aspx

OpenMP程序设计的两个小技巧
1、动态设置并行循环的线程数量
在实际情况中,程序可能运行在不同的机器环境里,有些机器是双核,有些机器是4核甚至更多核。并且未来硬件存在升级的可能,CPU核数会变得越来越多。如何根据机器硬件的不同来自动设置合适的线程数量就显得很重要了,否则硬件升级后程序就得进行修改,那将是一件很麻烦的事情。
比如刚开始在双核系统中开发的软件,线程数量缺省都设成2,那么当机器升级到4核或8核以后,线程数量就不能满足要求了,除非修改程序。
线程数量的设置除了要满足机器硬件升级的可扩展性外,还需要考虑程序的可扩展性,当程序运算量增加或减少后,设置的线程数量仍然能够满足要求。显然这也不能通过设置静态的线程数量来解决。
在具体计算需要使用多少线程时,主要需要考虑以下两点:
1) 当循环次数比较少时,如果分成过多数量的线程来执行,可能会使得总运行时间高于较少线程或一个线程执行的情况。并且会增加能耗。
2) 如果设置的线程数量远大于CPU核数的话,那么存在着大量的任务切换和调度等开销,也会降低整体效率。
那么如何根据循环的次数和CPU核数来动态地设置线程的数量呢?下面以一个例子来说明动态设置线程数量的算法,假设一个需要动态设置线程数的需求为:
1、 以多个线程运行时的每个线程运行的循环次数不低于4次
2、 总的运行线程数最大不超过2倍CPU核数
下面代码便是一个实现上述需求的动态设置线程数量的例子
const int MIN_ITERATOR_NUM = 4;
int ncore = omp_get_num_procs(); //获取执行核的数量
int max_tn = n / MIN_ITERATOR_NUM;
int tn = max_tn > 2*ncore ? 2*ncore : max_tn; //tn表示要设置的线程数量
#pragma omp parallel for if( tn > 1) num_threads(tn)
for ( i = 0; i < n; i++ )
{
printf("Thread Id = %ld/n", omp_get_thread_num());
//Do some work here
}

在上面代码中,根据每个线程运行的循环次数不低于4次,先计算出最大可能的线程数max_tn,然后计算需要的线程数量tn,tn的值等于max_tn和2倍CPU核数中的较小值。
然后在parallel for构造中使用if子句来判断tn是否大于1,大于1时使用单个线程,否则使用tn个线程,,这样就使得设置的线程数量满足了需求中的条件。
比如在一个双核CPU上,n=64,最终会以2倍CPU核数(4个)线程运行,而不会以max_tn = 64/4=16个线程运行。
在实际情况中,当然不能每个循环都象上面一样写几行代码来计算一遍,可以将其写成一个独立的功能函数如下:
const int g_ncore = omp_get_num_procs(); //获取执行核的数量

/** 计算循环迭代需要的线程数量
根据循环迭代次数和CPU核数及一个线程最少需要的循环迭代次数
来计算出需要的线程数量,计算出的最大线程数量不超过CPU核数

@param int n - 循环迭代次数
@param int min_n - 单个线程需要的最少迭代次数
@return int - 线程数量
*/
int dtn(int n, int min_n)
{
int max_tn = n / min_n;
int tn = max_tn > g_ncore ? g_ncore : max_tn; //tn表示要设置的线程数量
if ( tn < 1 )
{
tn = 1;
}
return tn;
}
这样每次并行化循环时就可以直接使用函数dtn()来获取合适的线程数量,前面的代码可以简写成如下形式:
#pragma omp parallel for num_threads(dtn(n, MIN_ITERATOR_NUM))
for ( i = 0; i < n; i++ )
{
printf("Thread Id = %ld/n", omp_get_thread_num());
//Do some work here
}

当然具体设置多少线程要视情况而定的,一般情况下线程数量刚好等于CPU核数可以取得比较好的性能,因为线程数等于CPU核数时,每个核执行一个任务,没有任务切换开销。
2、嵌套循环的并行化
在嵌套循环中,如果外层循环迭代次数较少时,如果将来CPU核数增加到一定程度时,创建的线程数将可能小于CPU核数。另外如果内层循环存在负载平衡的情况下,很难调度外层循环使之达到负载平衡。
下面以矩阵乘法作为例子来讲述如何将嵌套循环并行化,以满足上述扩展性和负载平衡需求。
一个串行的矩阵乘法的函数代码如下:
/** 矩阵串行乘法函数
@param int *a - 指向要相乘的第个矩阵的指针
@param int row_a - 矩阵a的行数
@param int col_a - 矩阵a的列数
@param int *b - 指向要相乘的第个矩阵的指针
@param int row_b - 矩阵b的行数
@param int col_b - 矩阵b的列数
@param int *c - 计算结果的矩阵的指针
@param int c_size - 矩阵c的空间大小(总元素个数)
@return void - 无
*/
void Matrix_Multiply(int *a, int row_a, int col_a,
int *b, int row_b,int col_b,
int *c, int c_size)
{
if ( col_a != row_b || c_size < row_a * col_b )
{
return;
}

int i, j, k;
//#pragma omp for private(i, j, k)
for ( i = 0; i < row_a; i++ )
{
int row_i = i * col_a;
int row_c = i * col_b;
for ( j = 0; j < col_b; j++ )
{
c[row_c + j] = 0;
for ( k = 0; k < row_b; k++ )
{
c[row_c + j] += a[row_i + k] * b[k * col_b + j];
}
}
}
}
如果在外层循环前加上OpenMP的for语句时,它就变成了一个并行的矩阵乘法函数,但是这样简单地将其并行化显然无法满足前面所述的扩展性需求。
其实可以采用一个简单的方法将最外层循环和第2层循环合并成一个循环,下面便是采用合并循环后的并行实现。

void Parallel_Matrix_Multiply(int *a, int row_a, int col_a,
int *b, int row_b,int col_b,
int *c, int c_size )
{
if ( col_a != row_b )
{
return;
}

int i, j, k;
int index;
int border = row_a * col_b;

i = 0;
j = 0;
#pragma omp parallel private(i,j,k) num_threads(dtn(border, 1))
for ( index = 0; index < border; index++ )
{
i = index / col_b;
j = index % col_b;

int row_i = i * col_a;
int row_c = i * col_b;

c[row_c+j] = 0;
for ( k = 0; k < row_b; k++ )
{
c[row_c + j] += a[row_i+k] * b[k*col_b+j];
}
}
}
从上面代码可以看出,合并后的循环边界border = row_a * col_b;即等于原来两个循环边界之积,然后在循环中计算出原来的外层循环和第2层循环的迭代变量i和j,采用除法和取余来求出i和j的值。
需要注意的是,上面求i和j的值必须要保证循环迭代的独立性,即不能有循环迭代间的依赖关系。不能将求i和j值的过程优化成如下的形式:
if ( j == col_b )
{
j = 0;
i++;
}
// …… 此处代表实际的矩阵乘法代码
j++;
上面这种优化,省去了除法,效率高,但是只能在串行代码中使用,因为它存在循环迭代间的依赖关系,无法将其正确地并行化。

你可能感兴趣的:(OpenMP编程指南)