PROGRAM HELLO
!$OMP PARALLEL
PRINT *,”Hello World”
!$ OMP END PARALLEL
intel: ifort -openmp -o hi.x hello.f
pgi: pgfortran -mp -o hi.x hello.f
gnu: gfortran -fopenmp -o hi.x hello.f
Export OMP_NUM_THREADS=4
./hi.x
FORTRAN指令格式:
!$ OMP PARALLEL [clauses]
:
!$OMP END PARALLEL
OpenMP 遵循Fork/Join模型
OpenMP程序从一个线程开始;主线程(线程0)
在并行区域开始时,master创建一组并行“worker”线程(FORK)
并行块中的语句由每个线程并行执行
在并行区域的末尾,所有线程同步(隐式屏障implicit barrier),并连接主线程(JOIN)
OpenMP线程与内核
线程是独立的程序代码执行序列
代码块只有一个入口和一个出口
例如 !$OMP PARALLEL PRIVATE(a,b,c) (fortran)
在OpenMP中,默认情况下,大多数变量都认为是共享的。以下情况例外:指针变量(Fortran, C/C++)和在parallel region区内声明的变量(C/C++)
TIPS:实际问题
openmp为每个工作线程创建单独的数据堆栈以存储私有变量的副本(主线程使用常规堆栈)
OpenMP标准未定义这些堆栈的大小
英特尔编译器:默认堆栈为4MB
gcc/gfortran:默认堆栈为2mb
超出堆栈空间时,程序的行为未定义
尽管大多数编译器/RT都会抛出seg fault
要增加堆栈大小,请使用环境变量OMP_STACKSIZE,例如
OpenMP帮你划分好了迭代空间。你唯一需要做的就是添加!$OMP DO 和!$OMP END DO指令
“可以通过使用 !$OMP PARALLEL DO 指令 使得命令更加紧凑”
规约 Reductions
a = a 运算符 表达式 称为归约运算。显然,变量“a”带有一个 流依赖关系(稍后将讨论),它阻碍了了并行化。
对于此类情况,openmp提供了规约语句 REDUCTION(op:list)。语句适用于满足下列限制条件时:
a是列表中的标量变量
expr是一个标量表达式,它不引用a
只允许某些类型的运算符;例如,+,*,。-
在fortran中,运算符也可以是内置函数的;例如MAX,MIN,IOR
列表中的变量必须是共享的
提示:openmp作用域规则
到目前为止,所有指令都嵌套在同一个例程中(最外层的!$OMP PARALLEL)。然而,OpenMP提供了更灵活的范围规则。它只允许有例行程序!$OMP DO在这种情况下,我们调用!$OMP DO执行孤立指令orphaned directive。
注意:有一些规则(例如,当遇到一个!$OMP DO directive,程序应在parallel部分)
OMP如何安排迭代?
尽管openmp标准没有指定循环应该如何分区,但默认情况下,大多数编译器在N/p(N 迭代数,p线程数)块中分割循环。这称为静态调度(块大小为N/p)
为了显式地告诉编译器使用静态调度(或者我们稍后将看到的其他调度),openmp提供了SCHEDULE子句
$!OMP DO SCHEDULE (STATIC,n) (n是分块的大小)
静态调度的问题
在静态调度中,迭代次数在所有openmp线程中均匀分布(即每个线程将被分配相似的迭代次数)。这并不总是分割的最佳方式。这是为什么?
有了动态调度,新的块在线程可用时被分配给它们。OpenMP提供两个动态调度:
$!OMP DO SCHEDULE(DYNAMIC,n) // n is chunk size
1. S3→S2 anti(B) 变量B反依赖。对于变量B,写第i个的数据(S2),读第i+1个的数据(S3) (先写后读)
2. S3→S4 flow(A) 变量A流依赖。对于变量A,写第i+1个的数据(S3),读第i个的数据(S4)(先读后写)
3. S4→S2 flow(B) 变量temp流依赖。对于变量temp,先读取它的数据(S2),然后再写入数据(S4)(先读后写)
4. S3→S4 flow(B) 变量temp输出依赖。对于变量temp,先向它写入数据(S4),然后向它写入数据(下一个S4)(写后写)
循环携带的反依赖项和输出依赖项不是真正的依赖项(重复使用相同的名称),在许多情况下可以相对容易地解决。
流依赖项是真正的依赖项(有一个从定义到使用的流),在许多情况下不容易删除。可能需要重写算法(如果可能)
除了openmp之前描述的子句之外,一些非常有用的附加datascope子句:
FIRSTPRIVATE ( list ):
与private相同,但变量“x”的每个私有副本都是用“x”的原始值(在omp区域开始之前)初始化
LASTPRIVATE ( list ):
与private相同,但列表中上次工作共享的变量的私有副本将复制到共享版本。与!$OMP DO指令一起使用。
DEFAULT (SHARED | PRIVATE | FIRSTPRIVATE | LASTPRIVATE ):
指定omp区域中所有变量的默认范围。
NOWAIT 指令
每当作业共享结构中的线程(例如!$OMP DO)比其他线程更快地完成工作,它将等待所有参与线程完成各自的工作。所有线程将在作业共享结构结束时同步。
对于不需要或不想在末尾同步的情况,OpenMP 提供了NOWAIT 指令
关于作业共享总结
我们讨论了OMP 作业共享结构
!$OMP DO
!$OMP SECTIONS
!$OMP SINGLE
!$OMP CRITICAL
此指令确保只有一个线程可以执行块中的代码。如果另一个线程到达临界区,它将等待直到当前线程完成此临界区。每个线程都将执行关键块,并且它们将在CRITICAL部分的末尾同步。
序列化关键块
如果临界区的时间相对较大→加速可忽略不计
!$OMP ATOMIC
这个指令非常类似于上一张幻灯片上的 !$OMP CRITICAL 指令。不同的是 !$OMP ATOMIC仅用于更新内存位置。有时 !$OMP ATOMIC被称为mini 临界区。
块仅由一个语句组成
原子语句必须遵循特定语法
在前面的示例中,可以将“critical”替换为“atomic”
!$OMP BARRIER
!$OMP BARRIER 将强制每个线程在该屏障处等待,直到所有线程都达到该屏障为止。!$OMP BARRIER 可能是最著名的同步机制;显式或隐式地。我们之前讨论过的以下omp指令包含一个隐式屏障:
!$ OMP END PARALLEL
!$ OMP END DO
!$ OMP END SECTIONS
!$ OMP END SINGLE
!$ OMP END CRITICAL
理想情况下,我们希望有完美的加速(即使用n个处理器时n的加速)。然而,这在大多数情况下并不切实可行,原因如下:
并非所有的执行时间都花在并行区域(例如循环)上
例如,并非所有循环都是并行的(数据依赖关系)
使用openmp线程有一个固有的开销
IF (logical expr)
$!OMP PARALLEL IF(n > 100000) (fortran)
#pragma omp parallel if (n>100000) (C/C++)
这将只在n>100000时运行并行区域
Amdahl法则
每个程序由两部分组成::
显然,无论有多少个处理器,串行部分只在一个处理器执行。假设,程序花费总时间的p(0<p<1)部分在并行区域,(相对于串行的)运行时间将是:
((1-p)+p/N)/1
这意味着加速倍数将是:
1/((1-p)+p/N)
例如:假设80%的程序可以并行执行,且有用不限制的CPU数目,则最大加速将仅仅是5倍
OpenMP开销/可扩展性
启动并行OpenMP 区域不是免费的。这涉及相当多的开销。在循环周围放置openmp pragmas之前,请考虑以下内容:
记住阿姆达定律
尽可能并行化大多数外部循环(在某些情况下,即使迭代次数较少)
确保并行区域的加速足以克服开销
循环中的迭代次数足够大吗?
每次迭代的工作量是否足够?
不同机器/操作系统/编译器的开销可能不同
设置环境变量OMP_NESTED以启用/禁用嵌套
omp_get_nested(), omp_set_nested() 运行时函数
编译器仍然可以选择序列化嵌套的并行区域(即只使用一个线程的team )
大量开销。为什么?
会导致额外的负载不平衡。为什么?