0%

openmp学习笔记

openmp学习笔记

主要参考这两篇博客:
https://blog.csdn.net/ArrowYL/article/details/81094837
https://blog.csdn.net/YUNXIN221/article/details/103964460
在此向两位博主表示感谢。

几种常用的子句

openmp 指令一般以 #pragma omp 开头,后面接其他的子句。常用子句有:

parallel

用于指定接下来的代码块被并行执行,如
image.png
但如果把大括号去掉,则只有第一句被并行执行:
image.png

for

for子句一般与parallel一起用,可以使一段for循环被分配到多线程处理。注意,用户需要保证for循环中没有数据依赖问题,否则会得到错误结果。
正确示范:
image.png

错误示范:

1
2
3
4
5
6
7
8
9
10
#include<bits/stdc++.h>
#include<omp.h>
using namespace std;
int main(void){
int fib[101]={1,1};
#pragma omp parallel for
for(int i=2;i<=20;i++)fib[i]=fib[i-1]+fib[i-2];

for(int i=0;i<=20;i++)printf("%d ",fib[i]);
}

结果为
image.png
可以看到,只有前几项是正确的结果,后面的数都是0,这是因为openmp将for循环分给多个线程执行,后面的线程执行的时候fib[i-1]和fib[i-2]还没被算出来。

此外,需注意的是使用for子句还对for循环有很多要求:
1/循环的变量变量(就是i)必须是整形,其他的(double等)都不行。
2.循环的比较条件必须是< <= > >=中的一种
3.循环的增量部分必须是增减一个不变的值(即每次循环是不变的)。
4.如果比较符号是< <=,那每次循环i应该增加,反之应该减小
5.循环必须是没有奇奇怪怪的东西,不能从内部循环跳到外部循环,goto和break只能在循环内部跳转,异常必须在循环内部被捕获。

sections和section

sections 子句将每个section分给一个线程执行,不同section之间是并行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<bits/stdc++.h>
#include<omp.h>
int main(void){
#pragma omp parallel sections//注意这里一定要换行不然会报错
{
#pragma omp section
{//这里也要换行
printf("section 1 threadid=%d\n",omp_get_thread_num());
}
#pragma omp section
{
printf("kjaenknfe\n");
}
#pragma omp section
{
printf("section 3 threadid=%d\n",omp_get_thread_num());
}
#pragma omp section
{
printf("section 4 threadid=%d\n",omp_get_thread_num());
}
}
}

private,firstprivate,lastprivate

private(x)子句为每个线程声明一个私有变量x,不同线程的x之间没有联系,即使前面的程序中有x这个变量也不会干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<bits/stdc++.h>
#include<omp.h>
using namespace std;
int main(void){
int x=100;
cout<<"x="<<x<<endl;
#pragma omp parallel private(x)
{
x=omp_get_thread_num();
printf("In thread %d,x=%d\n",omp_get_thread_num(),x);
}
cout<<"x="<<x<<endl;
}

运行结果:
image.png
firstprivate(x)子句使子线程的x继承主线程中的x值,不同线程之间的x没有联系,而子线程中改变x的值并不会导致主线程中的x的值改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<bits/stdc++.h>
#include<omp.h>
using namespace std;
int main(void){
int x=100;
cout<<"x="<<x<<endl;
#pragma omp parallel firstprivate(x)
{
x+=omp_get_thread_num();
printf("In thread %d,x=%d\n",omp_get_thread_num(),x);
}
cout<<"x="<<x<<endl;
}

运行结果:
image.png
lastprivate(x)子句的作用与firstprivate相反,它为每个子线程中声明变量x,但x并不继承主程序中x的值。当子线程结束之后,把子线程的x的值赋给主线程中的x。

在for循环中使用lastprivate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<bits/stdc++.h>
#include<omp.h>
using namespace std;
int main(void){
int x=100;
cout<<"x="<<x<<endl;
#pragma omp parallel for firstprivate(x),lastprivate(x)
for(int i=0;i<20;i++){
x+=i;
printf("%d %d\n",omp_get_thread_num(),x);
}

cout<<"x="<<x<<endl;
}

注意,如果是在for循环中使用lastprivate(x),则会将最后一个线程的x的最终值赋给主变量中的x。这里的最后一个线程指的是逻辑上的最后一个,而不是最后一个结束的,如这里线程号为11的线程是最后一个,它的x的值最终是119,所以把119赋给主程序中的x。

image.png

在sections中使用lastprivate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<bits/stdc++.h>
#include<omp.h>
using namespace std;
int main(void){
int x=100;
cout<<"x="<<x<<endl;
#pragma omp parallel sections lastprivate(x)
{
#pragma omp section
{
x=omp_get_thread_num();
printf("section 1:x=%d\n",x);
}
#pragma omp section
{
x=omp_get_thread_num();
printf("section 2:x=%d\n",x);
}
#pragma omp section
{
x=omp_get_thread_num();
printf("section 3:x=%d\n",x);
}
#pragma omp section
{
x=omp_get_thread_num();
printf("section 4:x=%d\n",x);
}
}

cout<<"x="<<x<<endl;
}

而如果是在sections里使用,则会把最后一个section的x赋值回去。
image.png

threadprivate

threadprivate(x)将x(一般是全局变量)复制到各个子线程,在其他线程中修改x的值不会影响主程序中x的值,但第0个线程中修改x的值会导致主程序中x的值一并修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<bits/stdc++.h>
#include<omp.h>
using namespace std;
int global_x = 10;
#pragma omp threadprivate(global_x)
int main(void){
#pragma omp parallel for
for(int i=0;i<100;i++){
global_x=i;
}

#pragma omp parallel
printf("in thread%d: %d\n",omp_get_thread_num(),global_x);

cout<<"In main thread: "<<global_x<<endl;

}

运行结果:
image.png

copyin

copyin(x)等于把threadprivate变量x全局变量广播一遍,让所有线程的x值与主进程相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<omp.h>
#include<bits/stdc++.h>
using namespace std;
int A=100;
#pragma omp threadprivate(A)
int main(void){
omp_set_dynamic(0);
#pragma omp parallel for
for(int i=0;i<100;i++){
A=i;
}
#pragma omp parallel
{
printf("The value of A in thread without copyin %d: %d\n",omp_get_thread_num(),A);
}
#pragma omp parallel copyin(A)
{
printf("The value of A in thread %d: %d\n",omp_get_thread_num(),A);
}
}

运行结果:
image.png

openmp中的任务调度问题

对于一个for循环,编译器要考虑将不同的部分调度给不同的线程执行。默认情况下openmp大多采用静态调度方法,即假设循环需要进行n次,有k个线程,则前$n%k$个线程执行 $\left\lceil\dfrac{n}{k}\right\rceil$次,后$n-n%k$个线程执行$\left\lfloor\dfrac{n}{k}\right\rfloor$次。

OpenMP提供了schedule子句来实现任务的调度。schedule子句格式:schedule(type,[size])。

  参数type是指调度的类型,可以取值为static,dynamic,guided,runtime四种值。其中runtime允许在运行时确定调度类型,因此实际调度策略只有前面三种。

1 静态调度static
如果参数[size]为空,则采用默认的调度方式;否则将循环的前size次由第一个线程执行,size+1 —— 2size 次由第二个线程执行……第(k-1)size+1——k*size次由第k个线程执行。如果k*size<n,则再分配size次给第一个线程、分配size个给第二个线程……

1
2
3
4
5
6
7
8
9
#include<bits/stdc++.h>
#include<omp.h>
using namespace std;
int main(void){
#pragma omp parallel for schedule(static,3)
for(int i=0;i<50;i++){
printf("%d: Thread %d\n",i,omp_get_thread_num());
}
}

运行结果:
image.png

2 动态调度dynamic
动态调度依赖于运行时的状态动态确定线程所执行的迭代,也就是线程执行完已经分配的任务后,会去领取还有的任务(与静态调度最大的不同,每个线程完成的任务数量可能不一样)。由于线程启动和执行完的时间不确定,所以迭代被分配到哪个线程是无法事先知道的。

  当不使用size 时,是将迭代逐个地分配到各个线程。当使用size 时,逐个分配size个迭代给各个线程,这个用法类似静态调度。

3 启发式调度guided
  采用启发式调度方法进行调度,每次分配给线程迭代次数不同,开始比较大,以后逐渐减小。开始时每个线程会分配到较大的迭代块,之后分配到的迭代块会逐渐递减。迭代块的大小会按指数级下降到指定的size大小,如果没有指定size参数,那么迭代块大小最小会降到1。

  size表示每次分配的迭代次数的最小值,由于每次分配的迭代次数会逐渐减少,少到size时,将不再减少。具体采用哪一种启发式算法,需要参考具体的编译器和相关手册的信息。

一点小建议

尽量不要用在并行执行的程序块里cout。原因很简单,比如cout<<a<<b,可能第一个线程刚输出了a还没输出b,第二个线程由输出了a,就乱套了,比如下面的例子:

1
2
3
4
5
6
7
#include<bits/stdc++.h>
#include<omp.h>
using namespace std;
int main(void){
#pragma omp parallel for
for(int i=0;i<10;i++)cout<<"Thread:"<<omp_get_thread_num()<<"\n";
}

image.png

但如果是使用printf就这个问题:
image.png

如果要统计一段openmp并行化的程序的运行时间,不要用clock()函数。这是因为clock()得到的是cpu时间,会把多个核的时间加在一起;而我们想要的一般是现实中经过的时间,所以要用omp_get_wtime()函数。
举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<bits/stdc++.h>
#include<omp.h>
using namespace std;
int x;
void f(){
for(int i=0;i<300000000;i++);
}
int main(void){
double t1=omp_get_wtime();
#pragma omp parallel for
for(int i=0;i<12;i++)f();
double t2=omp_get_wtime();
cout<<t2-t1<<"s\n";

double t3=clock();
#pragma omp parallel for
for(int i=0;i<12;i++)f();
double t4=clock();
cout<<(t4-t3)/1e6<<"s\n";
}

结果如下:
image.png
用clock()统计出来的时间有6.9s,但事实上程序运行的时间不到2s。openmp在我的电脑上默认使用12线程,所以大概是clock()函数会把每个线程的时间都加起来。