并行程序设计导论知识点整理
第一章 为什么要并行计算
- 任务并行:将待解决的问题所需要执行的各个任务分配到核上进行。
- 数据并行:将待解决的问题需要处理的数据分配给各个核,每个核在分配到的数据集上执行大致相似的操作。
- 编写显式并行程序:消息传递接口MPI、POSIX线程、OpenMP
- 主要并行系统:共享内存系统、分布式系统
- 共享内存系统:理论上每个核能够读、写内存的所有区域。可以通过检测和更新共享内存中的数据来协调各个核。(Pthreads,OpenMp)提供访问共享内存的机制。
- 分布式内存系统(MPI):每个核拥有自己的私有内存,核之间的通信是显示的(利用类似在网络中发消息的机制)(MPI)提供发送消息的机制
- 并行程序往往在多个核上执行多个任务,核通过共享内存或高速网络来连接。
- 编写并行程序涉及:核之间的通信、负载平衡、同步。
第二章 并行硬件和并行软件
串行系统:
- 冯诺依曼结构体系的瓶颈:CPU与主存分离
- 改进:使用Cache高速缓存
- 局部性:在访问完一个内存区域的数据或指令,程序将在不久的将来访问其邻近区域(空间局部性)
- CPU缓存是在CPU寄存器与主存之间的中间存储器,主要目的是降低主存访问的延迟。
- 主存可以看作是二级存储器的缓存。主存中只保留程序指令和数据的活动部分,其余所有部分都保存在二级存储的交换空间里。
- 由于使用页表,程序两次访问主存 (1) 获取页表项,得到虚拟地址对应的物理地址。(2) 对需要的数据实际访问。 为避免主存访问次数增加,CPU设置特殊的页表缓存(转译后备缓冲器TLB),存储最近使用到的页表项。
- 指令级并行(ILP)单进程同时执行多个指令流水线、多发射
- 线程级并行(TLP)比ILP更加粗粒度。
并行硬件:
- Flynn分类法对并行硬件进行分类:
a. SISD(单指令单数据流):一次执行一条指令,存取一个数据项
b. SIMD(单指令多数据流):系统任意时间执行一条指令,但该指令可对多个数据项进行操作。通常使用数据并行。对分支指令的处理主要是:让那些没有对数据项使用分支指令的处理器处于空闲状态。
c. MIMD(多指令多数据流):系统同时执行多个指令流,每个指令流都有自己的数据流。 - 处理器和内存之间的连接方式:
a. 共享内存系统:总线、交叉开关矩阵
b. 分布式系统:直接连接,二维环面网格和超立方体。 - 缓存一致性问题(共享内存):相同变量存储在不同核的缓存中,若其中之一发生变化,另一个核却不知道。
- 解决缓存一致性问题:监听和使用目录。监听需要依靠互联结构从一个缓存控制器向其他缓存控制器广播信息的能力。目录存储每一条缓存行的信息。伪共享。
并行软件:
关注同构MIMD系统的软件开发。单个程序,通过分支语句实现并行。
SPMD单程序多数据流。消息传递接口MPI
- 性能:线性加速比、效率
- 阿姆达尔定律:除非一个串行程序的执行全部都并行化,否则不论有多少可以利用的核,通过并行化所产生的加速比都会是受限的。如果r代表串行程序的“天然串行”部分,即无法并行化所占的比例,我们不可能获得比1/r更好的加速比。
- 可扩展性:一个并行程序,如果问题的规模与进程/线程数以一定的倍率增加,而效率保持一个常数值,那么该并行程序就是可扩展的。问题规模保持不变-强可扩展,问题规模和线程/进程数量等倍率增长-弱可扩展。
并行程序设计:
Foster方法(1)划分问题识别任务;(2)在任务中识别要执行的通信;(3)凝聚或聚合任务使之变成较大的组任务;(4)将聚合任务分配给进程/线程。
第三章 用MPI进行分布式内存编程
本章将讨论如何使用消息传递对分布式内存系统编程。
- MPI程序典型的基本框架:
#include<mpi.h>
int main(int argc,char* argv[]){
MPI_Init(&argc,&argv);
…
MPI_Finalize();
return 0;
}
MPI_Init()告知MPI系统进行所有必要的初始化设置。例如为消息缓冲区分配存储空间,为进程指定进程号等。
MPI_Finalize()告知系统MPI使用完毕,可以释放资源。
- 通信子,MPI_Comm_size和MPI_Comm_rank
通信子:一组可以互相发送消息的进程集合。
MPI_Init()在用户启动程序时,定义由用户启动的所有进程所组成的通信子MPI_COMM_WORLD。
获取MPI_COMM_WORLD信息:
Int MPI_Comm_size(comm,comm_sz);
Int MPI_Comm_rank(comm,my_rank);
comm表示通信子,为MPI定义的特殊类型MPI_Comm
comm_sz表示通信子的进程数,my_rank返回正在调用的进程在通信子中的进程号。
3. SPMD程序—让进程按照他们的进程号来匹配程序分支。(单程序多数据流)。
- MPI_Send
语法结构:
int MPI_Send(
void* msg_buf_p, 包含消息内容的内存块指针(grteeting)
int msg_size, 消息大小
MPI_DataType msg_type, 消息类型(MPI_CHAR,MPI_INT…)
Int dest, 接受进程的进程号
Int tag, 区分消息
MPI_Comm communicator 通信子-指定通信范围 );
- MPI_Recv
语法结构:
int MPI_Recv(
void* msg_buf_p, 包含消息内容的内存块指针(grteeting)
int buf_size, 消息大小
MPI_DataType buf_type, 消息类型(MPI_CHAR,MPI_INT…)
Int source, 发送进程的进程号
Int tag, 区分消息
MPI_Comm communicator 通信子-指定通信范围
MPI_Status* status_p );
status_p大多数情况下,调用函数并不使用,赋予MPI_STATUS_IGNORE即可。
- 消息匹配
q号进程调用的send函数发送的消息可以被r号进程调用recv函数接收,如果:
recv_comm=send_comm,
recv_tag=send_tag,
dest=r,
source=q.
并且recv_type=send_type,recv_buf_sz≥send_buf_sz.那么消息可以传递成功。
一个进程可以接受多个进程发来的消息,接收进程并不知道其他进程发送消息的顺序。如果0号进程按照进程号顺序接收消息,那么,当一个靠后进程首先完成任务,有可能会等待其他进程完成。为避免该问题将MPI_ANY_SOURCE常量传递给MPI_Recv,这样,0号进程即可按照任务完成的顺序接收结果。
同理,一个进程若接收多条来自另一个进程有着不同标签的消息可以使用MPI_ANY_TAG规避以上问题。
for (i=1;i<comm_sz;i++){
MPI_Recv(result,result_sz,MPI_INT,MPI_ANY_SOURCE, MPI_ANY_TAG,comm, MPI_STATUS_IGNORE)
}
使用通配符时应注意:
(1)只有接收者可以使用。
(2)通信子参数没有。
- MPI实现梯形积分法
串行化梯形积分伪代码:
h=(b-a)/n;
approx=(f(a)+f(b))/2.0;
for(i=1;i<n,i++){
x_i=a+i*h;
approx+=f(x_i);
}
approx=h*approx;
并行化梯形积分法:串->并
(1) 将问题的解决方案划分为多个任务
(2) 在任务见识别需要通信的通道
(3) 将任务聚合成复合任务
(4) 在核上分配复合任务
简单假设进程数可以整除梯形个数,则并行程序的伪代码为:
h=(b-a)/n; local_n=n/comm_sz;//每个进程需要计算梯形面积的个数
local_a=a+my_rank*local_n*h;//该进程计算梯形集合的左端点坐标
local_b=local_a+ local_n*h;//该进程计算梯形集合的右端点坐标
local_integral=Trap(local_a,local_b,local_n,h);//积分法求面积
if(my_rank!=0){
Send local_integral to process 0;
}else{
total_integral=local_integral;
for(i=1;i<comm_sz;i++){
receive local_integral from i;
total_integral+= local_integral;
}
}
If(my_rank==0)
Print result;
梯形积分法MPI程序:
double Trap(
double left_endpt,
double right_endpt,
int trap_count,
double base_len
){
double estimate,x;
int i;
estimate=(f(left_endpt)+f(right_endpt))/2.0;
for(i=1;i<=trap_count-1;i++){
x=left_endpt+i*base_len;
estimate+=f(x);
}
estimate=estimate*base_len;
return estimate;
}
int main(void){
int my_rank,comm_sz,n=1024,local_n;
double a=0.0,b=3.0,h,local_a,local_b;
double local_int,total_int;
int source;
MPI_Init(NULL,NULL);
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
MPI_Comm_sz(MPI_COMM_WORLD,&comm_sz);
MPI_COMM_WORLD comm;
h=(b-a)/n;
local_n=n/comm_sz;
local_a=a+my_rank*local_n*h;//该进程计算梯形集合的左端点坐标
local_b=local_a+ local_n*h;//该进程计算梯形集合的右端点坐标
local_int=Trap(local_a,local_b,local_n,h);//积分法求面积
if(my_rank!=0){
MPI_Send(&local_int,1,MPI_DOUBLE,0,0,comm);
}else{
total_int=local_int;
for(source=1;source<comm_sz;source++){
MPI_Recv(&local_int,1,MPI_DOUBLE,source,0,comm,MPI_STATUS_IGNORE);
total_int+=local_int;
}
}
if(my_rank==0){
printf(“with n=%d trapezoids,our estimate\n”,n);
printf(“of the int from %f to %f=%.l5e\n”,a,b,total_int);
}
MPI_Finalize();
return 0;
}
进程的输出顺序不可预测,产生这一现象的原因是MPI进程都在相互竞争,以取得对共享输出设备,标准输出的访问。这种竞争会导致不确定性,即每次运行的实际输出可能会变化。
0号进程负责读取数据,并将数据发送给其他进程。除0号进程发送数据,其他进程都只是接收方。
首先初始化my_rank和comm_sz
…
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);
Get_data(my_rank,comm_sz,&a,&b,&n);
…
---->用于读取用户输入的函数:
Void Get_input(
int my_rank;
int comm_sz;
double* a_p;
double* b_p;
int* n_p;
){
int dest;
if(my_rank==0){
printf(“Enter a,b,and n\n”);
scanf(“%lf %lf %d”,a_p,b_p,n_p);
for(dest=1;dest<comm_sz;dest++){
MPI_Send(a_p,1,MPI_DOUBLE,dest,0,comm);
MPI_Send(b_p,1,MPI_DOUBLE,dest,0,comm);
MPI_Send(n_p,1,MPI_INT,dest,0,comm);
}
}else{
MPI_Recv(a_p,1, MPI_DOUBLE,0,0,comm,MPI_STATUS_IGNORE);
MPI_Recv(b_p,1, MPI_DOUBLE,0,0,comm,MPI_STATUS_IGNORE);
MPI_Recv(n_p,1, MPI_INT,0,0,comm,MPI_STATUS_IGNORE);
}
}
---->每个进程打印一条消息:(没有for循环!!!MPI系统)
#include<stdio.h>
#include<mpi.h>
int main(void){
int my_rank;
int comm_sz;
MPI_Init(NULL,NULL);
MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
printf(“process %d print message!\n”,my_rank,comm_sz);
MPI_Finalize();
return 0;
}
- 集合通信
在MPI中,设计通信子中所有进程的通信函数称为集合通信。
(1) 树形结构通信:进程配对
(2) MPI_Reduce:
Void *input //输入
Void *output //输出
Int count //数量
MPI_datatype datatype //类型
MPI_OP operator //函数类型 MPI_SUM,MPI_MAX…
Int dest_process //目的进程号
MPI_Comm comm
(3) 集合通信:
a) 所有进程必须调用相同的集合通信函数。
b) 进程传递给集合通信MPI的参数必须是相容的。
c) 参数output只用在dest_process
d) 点对点通信函数是通过标签和通信子匹配的,集合通信函数只通过通信子和调用的顺序进行匹配。
(4) MPI_Allreduce:令通信子中的所有进程都存储结果。
(5) 广播
(6) 数据分发:块划分、循环划分、块-循环
(7) 散射:MPI_Scatter:
0号进程读取整个向量,但只将分量发送给需要分量的其他进程。
(8) 聚集:MPI_Gather:打印分布式向量、该函数将所有分量都收集到0号进程上,由0号进程将所有分量都打印出来。