文章目录
一、OpenMP和MPI的对比
1.1 线程与进程
进程:是资源分配的最小单位。是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。(资源分配)
线程:是CPU调度的最小单位。是进程的一个执行单元,是进程内可调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。(执行体)
- 一个程序至少有一个进程,一个进程至少有一个线程
- 线程的划分尺度小于进程,使得多线程程序的并发性高
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是进程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制(进程不能独立执行)
1.2 openMP和MPI的区别
openmp: 线程级(并行粒度)、共享存储、隐式(数据分配方式)、可扩展性差。
MPI: 进程级、分布式存储、显式、可扩展性好。
openmp优点:
- OpenMP相对于MPI而言更容易使用。
- OpenMp对原串行代码改动较小,可以保护代码原貌。
- 代码更容易理解和维护
- 允许渐进式并行化
openmp缺点:
- 所有线程共享内存空间,硬件制约较大
- 目前主要针对循环并行化
适用于:SMP,DSM机器,不适合于集群。
MPI优点:
- 无论硬件是否共享内存空间,都可以使用。(但是线程间不共享内存空间)
- 与OpenMP相比,可以处理规模更大的问题
- 每个线程有自己的内存和变量,这样不用担心冲突问题
MPI缺点:
- 算法上经常有较大改动(建立communication等)
- 编程复杂度较大,调试复杂。
- 需要解决通信延迟大和负载不平衡两个主要问题。
- MPI程序可靠性差,一个进程出问题,整个程序将错误。
适用于:各种机器设备
二、openMP的简单使用
2.1 openMP的原理
- 只需加上专用的pragma,编译器就可以自动将程序进行并行化。
- 当选择忽略这些pragma或不支持openMP,程序又可退化为通常的程序(一般为串行)。
2.1.1 基于线程的并行
- OpenMP仅通过线程来完成并行。
- 一个线程的运行是可由操作系统调用的最小处理单。
- 线程们存在于单个进程的资源中,没有了这个进程,线程也不存在了。
2.1.2 明确的并行
- OpenMP是一种显式(非自动)编程模型,为程序员提供对并行化的完全控制。
- 一方面,并行化可像执行串行程序和插入编译指令那样简单。
- 另一方面,像插入子程序来设置多级并行、锁、甚至嵌套锁一样复杂。
2.1.3 Fork-Join模型
- 所有的OpenML程序都以一个单个进程——master thread开始,master threads按顺序执行知道遇到第一个并行区域。
- Fork:主线程创造一个并行线程组。
- Join:当线程组完成并行区域的语句时,它们同步、终止,仅留下主线程。
2.1.4 数据范围
- 由于OpenMP时是共享内存模型,默认情况下,在共享区域的大部分数据是被共享的
- 并行区域中的所有线程可以同时访问这个共享的数据
- 如果不需要默认的共享作用域,OpenMP为程序员提供一种“显示”指定数据作用域的方法
2.1.5 嵌套并行
- API提供在其它并行区域放置并行区域(实际使用也可能不支持)
2.1.6 动态线程
- API为运行环境提供动态的改变用于执行并行区域的线程数(实际使用也可能不支持)
2.1.7 简单使用
并行Hello world
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int nthreads, tid;
/* Fork a team of threads giving them their own copies of variables */
#pragma omp parallel private(nthreads, tid)
{
/* Obtain thread number */
tid = omp_get_thread_num();
printf("Hello World from thread = %d\n", tid);
/* Only master thread does this */
if (tid == 0)
{
nthreads = omp_get_num_threads();
printf("Number of threads = %d\n", nthreads);
}
} /* All threads join master thread and disband */
return 0;
}
gcc test.cpp -o test -fopenmp -lstdc++
三、OpenMP的parallel
3.1 编译器指令
并行区域构造:
#pragma omp parallel [clause ...] newline
if (scalar_expression)
private (list)
shared (list)
default(shared | none)
firstprivate (list)
reduction (operator:list)
copyin (list)
num_threads (integer-expression)
structured_block
决定线程数的因素有多个,它们的优先级如下:
- if语句的值
- 设置num_threads语句
- 使用的omp_set_num_threads() 库函数
- 设置的OMP_NUM_THREADS 环境变量
注意:生成的线程编号为0~N,其中0是主线程(master thread)的编号
指令(directive):
指令 | 作用 |
---|---|
atomic | 内存位置将会原子更新 |
barrier | 线程在此等待,直到所有的线程都运行到此barrier。用来同步所有线程。 |
critical | 其后的代码块为临界区,任意时刻只能被一个线程运行。 |
flush | 所有线程对所有共享对象具有相同的内存视图 |
for | 用在for循环之前,把for循环并行化由多个线程执行。循环变量只能是整型 |
master | 指定由主线程来运行接下来的程序。 |
ordered | 指定在接下来的代码块中,被并行化的 for循环将依序运行(sequential loop) |
parallel | 代表接下来的代码块将被多个线程并行各执行一遍。 |
sections | 将接下来的代码块包含于将被并行执行的section块。 |
single | 之后的程序将只会在一个线程(未必是主线程)中被执行,不会被并行执行。 |
threadprivate | 指定一个变量是线程局部存储(thread local storage) |
从句(clause):
指令 | 作用 |
---|---|
copyin | 让threadprivate的变量的值和主线程的值相同。 |
copyprivate | 不同线程中的变量在所有线程中共享。 |
default | Specifies the behavior of unscoped variables in a parallel region. |
firstprivate | 对于线程局部存储的变量,其初值是进入并行区之前的值。 |
if | 判断条件,可用来决定是否要并行化。 |
lastprivate | 在一个循环并行执行结束后,指定变量的值为循环体在顺序最后一次执行时获取的值,或者#pragma sections在中,按文本顺序最后一个section中执行获取的值。 |
nowait | 忽略barrier的同步等待。 |
num_threads | 设置线程数量的数量。默认值为当前计算机硬件支持的最大并发数。一般就是CPU的内核数目。超线程被操作系统视为独立的CPU内核。 |
ordered | 使用于 for,可以在将循环并行化的时候,将程序中有标记 directive ordered 的部分依序运行。 |
private | 指定变量为线程局部存储。 |
reduction | Specifies that one or more variables that are private to each thread are the subject of a reduction operation at the end of the parallel region.reduction子句对列表中的每个变量执行简化操作。为每个线程创建并初始化每个列表变量的私有副本。在缩减结束时,reduce变量应用于共享变量的所有私有副本,最终结果将写入全局共享变量。 |
schedule | 设置for循环的并行化方法;有 dynamic、guided、runtime、static 四种方法。shared 指定变量为所有线程共享。 |
schedule(static, chunk_size) | 把chunk_size数目的循环体的执行,静态依序指定给各线程。 |
schedule(dynamic, chunk_size) | 把循环体的执行按照chunk_size(缺省值为1)分为若干组(即chunk),每个等待的线程获得当前一组去执行,执行完后重新等待分配新的组。 |
schedule(guided, chunk_size) | 把循环体的执行分组,分配给等待执行的线程。最初的组中的循环体执行数目较大,然后逐渐按指数方式下降到chunk_size。 |
schedule(runtime) | 循环的并行化方式不在编译时静态确定,而是推迟到程序执行时动态地根据环境变量OMP_SCHEDULE 来决定要使用的方法。 |
shared | 指定变量为所有线程共享。 |
openmp的库函数
指令 | 作用 |
---|---|
void omp_set_num_threads(int _Num_threads) | 在后续并行区域设置线程数,此调用只影响调用线程所遇到的同一级或内部嵌套级别的后续并行区域.说明:此函数只能在串行代码部分调用. |
int omp_get_num_threads(void) | 返回当前线程数目.说明:如果在串行代码中调用此函数,返回值为1. |
int omp_get_max_threads(void) | 如果在程序中此处遇到未使用 num_threads() 子句指定的活动并行区域,则返回程序的最大可用线程数量.说明:可以在串行或并行区域调用,通常这个最大数量由omp_set_num_threads()或OMP_NUM_THREADS环境变量决定. |
int omp_get_thread_num(void) | 返回当前线程id.id从1开始顺序编号,主线程id是0 |
int omp_get_num_procs(void) | 返回程序可用的处理器数. |
void omp_set_dynamic(int _Dynamic_threads) | 启用或禁用可用线程数的动态调整.(缺省情况下启用动态调整.)此调用只影响调用线程所遇到的同一级或内部嵌套级别的后续并行区域.如果 _Dynamic_threads 的值为非零值,启用动态调整;否则,禁用动态调整 |
int omp_get_dynamic(void) | 确定在程序中此处是否启用了动态线程调整.启用了动态线程调整时返回非零值;否则,返回零值 |
int omp_in_parallel(void) | 确定线程是否在并行区域的动态范围内执行.如果在活动并行区域的动态范围内调用,则返回非零值;否则,返回零值.活动并行区域是指 IF 子句求值为 TRUE 的并行区域. |
void omp_set_nested(int _Nested) | 启用或禁用嵌套并行操作.此调用只影响调用线程所遇到的同一级或内部嵌套级别的后续并行区域._Nested 的值为非零值时启用嵌套并行操作;否则,禁用嵌套并行操作.缺省情况下,禁用嵌套并行操作. |
int omp_get_nested(void) | 确定在程序中此处是否启用了嵌套并行操作.启用嵌套并行操作时返回非零值;否则,返回零值. |
void omp_init_lock(omp_lock_t * _Lock) | 初始化互斥锁 |
void omp_init_nest_lock(omp_nest_lock_t * _Lock) | 初始化一个嵌套互斥锁 |
void omp_destroy_lock(omp_lock_t * _Lock),void omp_destroy_nest_lock(omp_nest_lock_t * _Lock) | 结束一个(嵌套)互斥锁的使用并释放内存 |
void omp_set_lock(omp_lock_t * _Lock,void omp_set_nest_lock(omp_nest_lock_t * _Lock) | 获得一个(嵌套)互斥锁 |
void omp_unset_lock(omp_lock_t * _Lock),void omp_unset_nest_lock(omp_nest_lock_t * _Lock) | 释放一个(嵌套)互斥锁 |
int omp_test_lock(omp_lock_t * _Lock), int omp_test_nest_lock(omp_nest_lock_t * _Lock) | 试图获得一个(嵌套)互斥锁,并在成功时放回真(true),失败是返回假(false) |
double omp_get_wtime(void) | 获取wall clock time,返回一个double的数,表示从过去的某一时刻经历的时间,一般用于成对出现,进行时间比较. 此函数得到的时间是相对于线程的,也就是每一个线程都有自己的时间. |
double omp_get_wtick(void) | 得到clock ticks的秒数 |
环境变量:
指令 | 作用 | 样例 |
---|---|---|
OMP_SCHEDULE | 此变量的值确定如何在处理器上调度循环的迭代 | export OMP_SCHEDULE=“guided, 4” ,export OMP_SCHEDULE=“dynamic” |
OMP_NUM_THREADS | 设置执行期间要使用的最大线程数。 | export OMP_NUM_THREADS=8 |
OMP_DYNAMIC | 启用或禁用动态调整可用于执行并行区域的线程数。有效值为TRUE或FALSE。 | export OMP_DYNAMIC=TRUE |
OMP_PROC_BIND | 启用或禁用绑定到处理器的线程。有效值为TRUE或FALSE。 | export OMP_PROC_BIND=TRUE |
OMP_NESTED | 启用或禁用嵌套并行性。有效值为TRUE或FALSE。 | export OMP_NESTED=TRUE |
OMP_STACKSIZE | 控制创建(非主)线程的堆栈大小。 | export OMP_STACKSIZE=2000500B ,export OMP_STACKSIZE=“3000 k " ,export OMP_STACKSIZE=10M ,export OMP_STACKSIZE=” 10 M " ,export OMP_STACKSIZE=“20 m " ,export OMP_STACKSIZE=” 1G" ,export OMP_STACKSIZE=20000 |
OMP_WAIT_POLICY | 提供有关等待线程的所需行为的OpenMP实现的提示。兼容的OpenMP实现可能会也可能不会遵守环境变量的设置。有效值为ACTIVE和PASSIVE。ACTIVE指定等待线程应该主动处于活动状态,即在等待时消耗处理器周期。PASSIVE指定等待线程应该主要是被动的,即在等待时不消耗处理器周期。ACTIVE和PASSIVE行为的细节是实现定义的 | export OMP_WAIT_POLICY=ACTIVE ,export OMP_WAIT_POLICY=active ,export OMP_WAIT_POLICY=PASSIVE ,export OMP_WAIT_POLICY=passive |
OMP_MAX_ACTIVE_LEVELS | 控制嵌套活动并行区域的最大数量。此环境变量的值必须是非负整数。如果请求的OMP_MAX_ACTIVE_LEVELS值大于实现可以支持的嵌套活动并行级别的最大数量,或者该值不是非负整数,则程序的行为是实现定义的。 | export OMP_MAX_ACTIVE_LEVELS=2 |
OMP_THREAD_LIMIT | 设置要用于整个OpenMP程序的OpenMP线程数。此环境变量的值必须是正整数。如果请求的OMP_THREAD_LIMIT值大于实现可以支持的线程数,或者该值不是正整数,则程序的行为是实现定义的。 | export OMP_THREAD_LIMIT=8 |
3.2 实例
for
#include <omp.h>
#define N 1000
#define CHUNKSIZE 100
main(int argc, char *argv[]) {
int i, chunk;
float a[N], b[N], c[N];
/* Some initializations */
for (i=0; i < N; i++)
a[i] = b[i] = i * 1.0;
chunk = CHUNKSIZE;
#pragma omp parallel shared(a,b,c,chunk) private(i)
{
#pragma omp for schedule(dynamic,chunk) nowait
for (i=0; i < N; i++)
c[i] = a[i] + b[i];
} /* end of parallel region */
}
section
#include <omp.h>
#define N 1000
main(int argc, char *argv[]) {
int i;
float a[N], b[N], c[N], d[N];
/* Some initializations */
for (i=0; i < N; i++) {
a[i] = i * 1.5;
b[i] = i + 22.35;
}
#pragma omp parallel shared(a,b,c,d) private(i)
{
#pragma omp sections nowait
{
#pragma omp section
for (i=0; i < N; i++)
c[i] = a[i] + b[i];
#pragma omp section
for (i=0; i < N; i++)
d[i] = a[i] * b[i];
} /* end of sections */
} /* end of parallel region */
}
clause
#include <omp.h>
main(int argc, char *argv[]) {
int i, n, chunk;
float a[100], b[100], result;
/* Some initializations */
n = 100;
chunk = 10;
result = 0.0;
for (i=0; i < n; i++) {
a[i] = i * 1.0;
b[i] = i * 2.0;
}
#pragma omp parallel for \
default(shared) private(i) \
schedule(static,chunk) \
reduction(+:result)
for (i=0; i < n; i++)
result = result + (a[i] * b[i]);
printf("Final result= %f\n",result);
}
四、OpenMP的SIMD
4.1 SIMD
所谓的SIMD指令,指的是single instruction multiple data,即单指令多数据运算,其目的就在于帮助CPU实现数据并行,提高运算效率。[SIMD原理],适合于多媒体应用等数据密集型运算。(https://www.cnblogs.com/ncdxlxk/p/10126617.html)
4.2 openmp的simd使用
简单的例子
#pragma omp simd
for (i = 0; i < count; i++)
{
a[i] = a[i-1] + 1;
b[i] = *c + 1;
bar(i);
}
从句Clauses:
指令 | 作用 |
---|---|
simdlen(length) | 指定向量长度 |
safelen(length) | 指定向量依赖距离 |
linear(list[ : linear-step]) | 从循环归纳变量线性映射到数组订阅。 |
aligned(list[ : alignment]) | 数据对齐 |
private(list) | 私有数据 |
lastprivate(list) | 在一个循环并行执行结束后,指定变量的值为循环体在顺序最后一次执行时获取的值 |
reduction(reduction-identifier:list) | 指定自定义的缩减操作。 |
collapse(n) | 合并循环嵌套。 |
例子
#pragma omp simd simdlen(8)
for (i = 0; i < count; i++)
{
a[i] = a[i-1] + 1;
b[i] = *c + 1;
bar(i);
}
五、MPI
5.1 MPI简介
MPI是一个跨语言的通讯协议,用于编写并行计算机,支持点对点和广播。MPI的目标是高性能,大规模性,和可移植性。MPI在今天仍为高性能计算的主要模型。消息传递接口是一种编程接口标准,而不是一种具体的编程语言。简而言之,MPI标准定义了一组具有可移植性的编程接口。
5.2 MPI常用函数
-
int MPI_Init (int* argc ,char** argv[] )
该函数通常应该是第一个被调用的MPI函数用于并行环境初始化,其后面的代码到 MPI_Finalize()函数之前的代码在每个进程中都会被执行一次。
– 除MPI_Initialized()外, 其余所有的MPI函数应该在其后被调用。
– MPI系统将通过argc,argv得到命令行参数(也就是说main函数必须带参数,否则会出错)。 -
int MPI_Finalize (void)
– 退出MPI系统, 所有进程正常退出都必须调用。 表明并行代码的结束,结束除主进程外其它进程。
– 串行代码仍可在主进程(rank = 0)上运行, 但不能再有MPI函数(包括MPI_Init())。 -
int MPI_Comm_size (MPI_Comm comm ,int* size )
获得进程个数 size。
– 指定一个通信子,也指定了一组共享该空间的进程, 这些进程组成该通信子的group(组)。
– 获得通信子comm中规定的group包含的进程的数量。 -
int MPI_Comm_rank (MPI_Comm comm ,int* rank)
– 得到本进程在通信空间中的rank值,即在组中的逻辑编号(该 rank值为0到p-1间的整数,相当于进程的ID。) -
int MPI_Send( void *buff, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
–void *buff:你要发送的变量。
–int count:你发送的消息的个数(注意:不是长度,例如你要发送一个int整数,这里就填写1,如要是发送“hello”字符串,这里就填写6(C语言中字符串未有一个结束符,需要多一位))。
–MPI_Datatype datatype:你要发送的数据类型,这里需要用MPI定义的数据类型,可在网上找到,在此不再罗列。
–int dest:目的地进程号,你要发送给哪个进程,就填写目的进程的进程号。
–int tag:消息标签,接收方需要有相同的消息标签才能接收该消息。
–MPI_Comm comm:通讯域。表示你要向哪个组发送消息。 -
int MPI_Recv( void *buff, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)
–void *buff:你接收到的消息要保存到哪个变量里。
–int count:你接收消息的消息的个数(注意:不是长度,例如你要发送一个int整数,这里就填写1,如要是发送“hello”字符串,这里就填写6(C语言中字符串未有一个结束符,需要多一位))。它是接收数据长度的上界. 具体接收到的数据长度可通过调用MPI_Get_count 函数得到。
–MPI_Datatype datatype:你要接收的数据类型,这里需要用MPI定义的数据类型,可在网上找到,在此不再罗列。
–int dest:接收端进程号,你要需要哪个进程接收消息就填写接收进程的进程号。
–int tag:消息标签,需要与发送方的tag值相同的消息标签才能接收该消息。
–MPI_Comm comm:通讯域。
–MPI_Status *status:消息状态。接收函数返回时,将在这个参数指示的变量中存放实际接收消息的状态信息,包括消息的源进程标识,消息标签,包含的数据项个数等。
5.3 实例
#include <stdio.h>
#include <string.h>
#include "mpi.h"
void main(int argc, char* argv[])
{
int numprocs, myid, source;
MPI_Status status;
char message[100];
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
if (myid != 0) { //非0号进程发送消息
strcpy(message, "Hello World!");
MPI_Send(message, strlen(message) + 1, MPI_CHAR, 0, 99,
MPI_COMM_WORLD);
}
else { // myid == 0,即0号进程接收消息
for (source = 1; source < numprocs; source++) {
MPI_Recv(message, 100, MPI_CHAR, source, 99,
MPI_COMM_WORLD, &status);
printf("接收到第%d号进程发送的消息:%s\n", source, message);
}
}
MPI_Finalize();
} /* end main */