并行计算机的种类
并行计算机通常包含多颗自带高速缓存(Cache)的CPU,这些CPU需要一定数量的内存才能工作;同时,这些CPU通常需要借助于网络传递数据从而实现CPU之间的协同工作;数据的显示需要借助于显卡(GPU)。因此并行计算机通常分为四大部分:CPU、GPU、存储器和网络。
多核CPU
单核CPU的主频达到3GHz后,过高的功耗和高散热问题成为限制CPU频率提高的瓶颈。但多媒体、大规模科学计算等多个应用领域却对处理器性能不断提出更高的要求,多核CPU因而产生。
多核处理器,又称为片上多处理器或单芯片多处理器(Chip Multi-Processor,CMP),是指在一个芯片上集成多个处理器核。各种处理器核一般都具有固定的逻辑结构:指令级单元、执行单元、一级缓存(L1)、二级缓存(L2)、存储器及控制单元、总接线口等。
GPU
GPU包含比CPU更多的处理单元、更大的带宽,对于多媒体计算中大量的重复处理过程有着天生的优势。CPU与GPU架构的对比如下图所示:
从硬件设计上来讲,CPU由专为顺序串行处理而优化的几个核心组成。GPU由数以千计的更小、更高效的核心组成,这些核心专为同时处理所任务而设计。
注:GPU与下显卡是有区别的。GPU的范围更大:显卡一定是GPU,但GPU不一定是显卡。
存储器与的CPU的连接方式
- 共享存储系统
根据存储器与的CPU的连接方式可分为共享存储系统和分布存储系统。在共享存储系统中,所有CPU共同使用同一个存储器和输入输出(I/O)设备,并且一般通过总线连接,如下图所示。
在共享存储系统中,内存空间是统一编址的,可以被CPU所共享;CPU之间数据通信依靠CPU对具有相同地址的内存单元的访问来实现。但是当多颗CPU对同一地址的内存单元进行读写操作时,会出现访问冲突,即数据竞争。
- 分布存储系统
在分布存储系统中,每颗CPU均具有各自的存储器和输入输出设备所组成的计算节点;多个计算节点通过网络相互连接形成了分布存储系统。由于每颗CPU的计算结果都有自己的存储器,因此可以保证CPU访问存储器,不会出现访问冲突。另外在网络中增加计算节点比较方便,即系统的可拓展性能好。缺点是各节点间必须借助于网络相互通信,因此数据通信比较困难,必须借助于专门的通信方法。
数据的通信方式
根据数据通信方式, 可以将并行计算系统分为共享地址空间系统和消息传递系统两大系统。
-
共享地址空间系统
在共享地址空间系统中,存储器的地址空间是统一的,因此可称为单地址系统和共享存储多处理器(Shared Memory Multiprocessors, SMM)系统。根据CPU与存储器的连接方式可进一步分类。如果存储器是集中式的,那么所有处理器能够以相同的速度访问内存,称为对称共享内存多处理系统(Symmetric Shared-memory Multiprocessors 或 Symmetric Multiprocessors, SMP)或者均匀存储访问系统(Uniform Memory Acess,UMA)。如果是分布式的,则称为分布式共享内存系统(Distributed Shared-Memory,DSM)或非均匀存储访问系统(Nonuniform Memory Access,NUMA)。 -
消息传递系统
在消息传递系统中,每个计算节点都是独立的计算机系统,而每个节点的存储器均单独编址,因此同一个地址对应于多个存储器。节点间数据的传递不能通过本地节点的处理器直接访问其他节点的存储器来实现,而必须通过节点之间相互发送含有数据信息的消息来实现。这种通过发送包含数据的消息来实现数据通信的系统称为消息传递系统。它可分为大规模并行处理机系统(Massively Parallel Processor,MPP)和集群系统(Cluster)。
常见的并行计算硬件系统
指令和数据之间的工作方式
单指令流单数据流系统 | 具有一个处理器核的个人计算机 |
单指令多数据流系统 | 在多颗CPU上运行相同的指令,但是每颗CPU所处理的数据对象并不相同 |
多指令单数据流系统 | 不存在 |
多指令多数据流系统 | 每颗CPU上执行的指令核处理的数据各不相同,常见的多核个人计算机和集群计算机 |
假设我们需要计算n个数的值再累加求和,如下是串行代码:
sum = 0;
for (i=0;i<n;i++){
x = Compute_next_value(...);
sum += x;
}
现在我们假设有 p p p个核,且 p p p远小于 n n n,那么每个核能够计算大约 n / p n/p n/p个数的值并累加求和,以得到部分和:
my_sum = 0;
my_first_i = ...;
my_last_i = ...;
for (my_i = my_first_i; my_i < my_last_i; my_i++){
my_x = Compute_next_value(...);
my_sum += my_x;
}
此处的前缀my_代表每个核都使用自己的私有变量,并且每个核能够独立于其他核来执行该代码块。
当每个核都计算完各自的my_sum值后,将自己的结果值发送给一个指定为"master"的核(主核),master核将收到的部分和累加而得到全局总和:
if (I'm the master core){
sum = my_x;
for each core other than myself{
receive value from core:
sum += value;
}
else{
send my_x to the master;
}
}
一个更好的办法是不再由一个master核计算所有部分和的累加工作,可以将各个核两两结对,如下图所示。其中,圆圈表示当前核所得到的和,箭头表示一个核将自己的部分和发送给另一个核,加号表示一个核在收到另一个核发送过的部分和后与自己本身的部分和相加。
- 协调工作
- 通信:一个核和多个和将自己的部分和结果发送给其他的核
- 负载均衡:希望给每个核分配大致相同的数据量进行计算,避免计算资源的浪费
- 同步
- 并发、并行、分布式
习题
串行代码
#include <iostream>
#include <math.h>
#define N 8
void foo(int S[]){
int last;
for (int divisor = 2, core_difference = 1; divisor <= N; divisor *= 2, core_difference *= 2){
for (int first = 0; first < N; first += divisor){
last = core_difference + first;
S[first] += S[last];
}
}
}
int main(){
//int S[n] = { 8, 19, 7, ... 13, 12, 14 };
int S[N] = { 8, 19, 7, 15, 7, 13, 12, 14 };
foo(S);
for (int i = 0; i < N; i++){
std::cout << S[i]<< std::endl;
}
}
并行伪代码
divisor = 2;
core_difference = 1;
sum = my_value;
while ( divisor <= number of cores )
{
if ( my_rank % divisor == 0 )
{
partner = my_rank + core_difference;
receive value from partner core;
sum += received value;
}
else
{
partner = my_rank - core_difference;
send my sum to partner core;
}
divisor *= 2;
core_difference *=2;
}
- 避免无法被2整除的改进
divisor = 2;
core_difference = 1;
sum = my_value;
while ( divisor <= number of cores )
{
if ( my_rank % divisor == 0 )
{
partner = my_rank + core_difference;
if (partner < number of cores)
{
receive value
sum += received value
}
}
else
{
partner = my_rank - core_difference;
send my sum to partner core;
}
divisor *= 2;
core_difference *=2;
}