openMP并不是只能对循环来并行的,循环并行化单独拿出来说是因为它在科学计算中非常有用,比如向量、矩阵的计算。所以我单独拿出这一部分给大家讲讲。这里主要讲解的是for循环。
编译指导语句:
一般格式:
#pragma omp parallel for [clause[clause…]]
for(index = first; qualification; index_expr)
{…}
第一句中[]的部分是可选的,由自己的程序并行特点而定。大家先不要把精力放到这里面。后面的文章中会继续讲解的。
并行化for的编写规则
1、index的值必须是整数,一个简单的for形式:for(int i = start; i < end; i++){…} 。
2、start和end可以是任意的数值表达式,但是它在并行化的执行过程中值不能改变,也就是说在for并行化执行之前,编译器必须事先知道你的程序执行多少次,因为编译器要把这些计算分配到不同的线程中执行。
3、循环语句只能是单入口但出口的。这里只要你避免使用跳转语句就行了。具体说就是不能使用goto、break、return。但是可以使用continue,因为它并不会减少循环次数。另外exit语句也是可以用的,因为它的能力太大,他一来,程序就结束了。
例子讲解
例1、for循环并行
#include
#include "omp.h"
int main(int argc, char* argv[])
{
int i;
#pragma omp parallel for
for (i = 0; i < 12; i++)
{ printf("i = %d %d/n", i, omp_get_thread_num()); }
return 0;
}
例1的执行结果如图1所示:
图1、例1的执行结果
从结果中可以看出 i 属于{0,1,2}时由0号线程执行,i 属于{3,4,5}时由1号线程执行,i 属于{6,7,8}时由2号线程执行,i 属于{9,10,11}时由3号线程执行。omp_get_thread_num()这个函数通过执行结果大家也知道了,他返回每个线程的编号。
并行编译子句
openMP中有多种并行化子句,这些子句都是为控制循环并行化编译而设定的,这里我们主要关注数据作用域子句,这里的数据作用域是指各个线程是否对某一变量有权访问。shared子句用来标记变量在各个线程之间是共享的,private子句标记变量在各个线程之间是私有的,实际上它会在在每个线程中保存一个副本。默认情况下,并行执行的变量是共享的。至于其它编译子句将在后面的文章中介绍。
用实例讲解数据作用域子句
实际上我很难想到一个综合的例子来讲解这种子句的限制异同,所以我写了几个例子。
例2、private
#include
#include "omp.h"
int main(int argc, char* argv[])
{
float x = 4.3f;
int i;
#pragma omp parallel for private(x)
for (i = 0; i < 12; i++)
{
x = 0;
printf("parallel x = %.1f, thread nummber:%d/n", x, omp_get_thread_num());
}
printf("/nserial x = %.1f, thread nummber:%d/n", x, omp_get_thread_num());
return 0;
}
图2、例2执行结果
例3 firstprivate(var):指定var在每个线程中都有一个副本,并且var的初始值在并行执行开始之前定义,每个并行线程的var的副本初值就是串行时定义的初始值。程序结束后串行程序中的var值并不会改变。
#include
#include "omp.h"
int main(int argc, char* argv[])
{
float x = 4.3f;
int i;
#pragma omp parallel for firstprivate(x)
for (i = 0; i < 12; i++)
{
x += 1.0f;
printf("parallel x = %.1f, thread nummber:%d/n", x, omp_get_thread_num());
}
printf("/nserial x = %.1f, thread nummber:%d/n", x, omp_get_thread_num());
return 0;
}
图3、例3的执行结果
例4 lastprivate(var):指定最后多线程执行完后原串行循环最后一次var的值带到主线程(串行部分)
#include
#include "omp.h"
int main(int argc, char* argv[])
{
float x = 4.3f;
int i;
#pragma omp parallel for lastprivate(x)
for (i = 0; i < 12; i++)
{
x = 0.0f;
printf("parallel x = %.1f, thread nummber:%d/n", x, omp_get_thread_num());
}
printf("/nserial x = %.1f, thread nummber:%d/n", x, omp_get_thread_num());
return 0;
}
图4、例4的执行结果
例5 firstprivate与lastprivate联用,很奇怪openMP很多情况下是不允许某个变量被指定两次规则的,他俩却可以,呵呵,而且配合效果还不错。
#include
#include "omp.h"
int main(int argc, char* argv[])
{
float x = 4.3f;
int i;
#pragma omp parallel for firstprivate(x) lastprivate(x)
for (i = 0; i < 12; i++)
{
x += (float)omp_get_thread_num();
printf("parallel x = %.1f, thread nummber:%d/n", x, omp_get_thread_num());
}
printf("/nserial x = %.1f, thread nummber:%d/n", x, omp_get_thread_num());
return 0;
}
图5、例5的执行结果
从上面的例2、3的程序中可以看出例2中每个线程中x都是私有的,它属于每个线程,在主线程的定义并不能带入到各个线程中,使用firstprivate后,x在主线程的初始值可以带到各个线程中,在图3可以看出每个线程x的输出结果实际是相同的,但是在并行执行结束后,主线程中的x值仍然为4.3。从例4的执行结果可以看出最后x的值带出到了主线程中,这个x值到底是哪个线程中的哪?答案是最后一句x赋值后的值,哪个线程执行完的最晚就是哪个x的值。例5显示firstprivate与lastprivate联合使用的执行结果。
例6 reduction规约操作,
执行reduction的变量要特别注意,以reduction(+:sum)为例。
第一种情况:sum为局部变量。这是你必须为sum在串行程序中赋初值,sum 被设置为每个线程私有,先各自执行完算出各自的sum值,最后主线程会将 《线程数+1》个sum变量规约,比如你num_thread(4),在开始并行执行之前你对规约变量赋初值为10,并行时假设每个线程算的的sum值为1,那么最终sum带到串行程序中的变量值为14(串行的10+四个线程的1)。
第二种情况:sum为全局变量。这是你不必为sum赋初始值,因为此时默认串行的sum值为0,进入每个线程的sum值也是0,规约时仍然是将《线程数+1》个sum值相加,因为你并没有对全局的sum赋初值,所以最后规约的结果看着像是只有各线程的sum参加了规约操作。其实当你将全局的sum赋初值时,你会发现最后规约的sum值又多加了全局变量sum的串行程序结果。
重要提醒:不管你怎样设计sum的串行声明形式,只要他在被定义为规约变量,每次进入并行线程的sum值都是0;
也许你想把每个并行线程的sum值初始化成一个非0的值,然后再各自线程中在使用,那么我可以告诉你,别想了(至少我没有做到)。因为我规约sum值,如果这个规约有意义你的每个线程应该是各自独立未回各自的sum的,那么这个初始值使用0就已经非常好了,因为各自的sum计算如果结果一样,你为何不直接用一句乘法哪(线程数*一个线程计算的sum值)。
#include
#include "omp.h"
int main(int argc, char* argv[])
{
float x = 0.0f;
int i;
float sum = 0.0f;
#pragma omp parallel for private(x) reduction(+:sum)
for (i = 0; i < 12; i++)
{
x = (float)omp_get_thread_num();
sum += x;
printf("parallel sum = %.1f, thread nummber:%d/n", sum, omp_get_thread_num());
}
printf("/nserial sum = %.1f, thread nummber:%d/n", sum, omp_get_thread_num());
return 0;
}
图6、例6执行结果
在例6中我使用了reduction(+:sum),这表示每个线程对sum这个共享变量执行加操作时其它任何线程不能对它进行加操作,实际上我们这样理解是有偏差的,真正的机理在执行结果中不难看出,实际每个线程都拷贝了一个sum的副本,先在自己执行时加完sum,等所有线程都执行结束后,主线程再将每个线程的sum副本的值加起来返回给主线程中sum。
小结
本节主要讲述了for语句的并行化。现在为止大家应该可以熟练使用for并行化了。文章中可能还有些不全面的地方,热切期望各位读者能给出批评和指正,期待中……