1. 概述

并行程序设计:将大任务分解为多个可同时执行的子任务,从而利用多核处理器多台计算机实现更高效的计算

  • 提高性能:缩短程序运行时间
  • 解决大规模数据问题:处理计算密集型任务

计算类型

  • 并发计算(cocurrent):一个程序的多个任务在同一个时间段内可以同时执行
  • 并行计算(parallel):一个程序通过多个任务紧密协作来解决某个问题
  • 分布式计算(distributed):一个程序需要与其他程序协作来解决某个问题

并行程序:专门设计用来在并行系统上运行的程序,从而可以充分发挥多核性能

  • 任务并行:不同操作在同一数据中同时执行,任务之间没有依赖关系
  • 数据并行:同一操作在不同数据上同时执行,任务之间具有因果关系

并行系统:在单个芯片上放置多个相对简单的处理器形成的集成电路

  • 共享内存:所有处理单元共享同一物理内存,通过直接访问共享内存实现进程/线程间的通信
  • 分布式内存:每个处理单元拥有独立的内存空间,数据交换需要通过网络进行,但通信延时和带宽可能会成为瓶颈

并行扩展

类型用途特点
消息传递接口(Message-Passing Interface, MPI)用于分布式内存系统,通过消息传递进行进程间通信适合大规模并行计算,但需要显式管理通信导致编程相对复杂
POSIX线程(Pthreads)用于共享内存系统,通过线程实现并行计算能够细粒度控制并行执行,但需要实现同步和互斥
OpenMP用于共享内存系统,基于编译器指令的方式实现并行化适合逐步并行化现有的串行代码,但实现十分复杂

2. 并行硬件

2.1 冯诺依曼体系

冯诺依曼体系:CPU 通过读取主存中的指令和数据来执行任务,计算机按照取指-译码-执行-存储这一顺序循环执行

指令级并行(Instruction-Level Parallelism, ILP):在同一时间内执行多条指令

  • 流水线(Pipelining):将指令的执行过程划分为多个连续的阶段,每个阶段由专门的硬件单元处理
  • 多发射(Multiple Issue):在一个时钟周期内发射多条不冲突的指令到不同的执行单元

线程级并行(Thread-Level Parallelism, TLP):在同一时间内执行多个线程

  • 硬件多线程(Hardware Multithreading, HMT):处理器支持在硬件层面管理和切换多个线程,使得当一个线程阻塞时,其他线程可以继续执行
  • 同步多线程(Simultaneous Multithreading, SMT):允许多个线程同时共享和利用处理器的各个执行单元

TLP提供的是粗粒度的并行性,ILP提供的是细粒度的并行性

2.2 并行系统

类型定义特点适用
单指令多数据流系统(Single Instruction Multiple Data, SIMD)一个控制单元向多个算术逻辑单元广播同一条指令,各 ALU 分别在不同的数据上执行这条指令指令同步执行,数据并行向量处理器,图像处理单元
多指令多数据流系统(Multiple Instruction Multiple Data, MIMD)每个处理单元都有自己的控制单元和算术逻辑单元,可以独立执行不同的指令流,对各自的数据进行操作独立运行,异步运行共享内存系统、分布式内存系统

2.3 互连网络

共享内存互连网络

  • 总线:一种公共通信介质,所有处理单元和内存共享同一传输线路,存在带宽竞争和访问冲突的问题
  • 交叉开关矩阵:采用交叉开关结构,每个处理单元与存储器/其他处理单元之间有独立连接通路

分布式内存互连网络

  • 直接互连:节点之间直接按照一定的拓扑结构(网格、环、环面、超立方体等)互联,每个节点只与相邻或部分特定节点直接相连
  • 间接互连:通过专用交换设备(路由器、交换机)将各节点连接起来,通信时数据需要经过一个或多个中间节点

3. 并行软件

3.1 并行程序

单程序多数据流(Single Program Multiple Data, SPMD):所有进程或线程运行同一份程序代码,但在不同的数据子集上工作

  1. 将任务在进程/线程之间分配:需要追求负载均衡通信量最小
  2. 安排进程/线程之间的同步:利用互斥锁、信号量等机制保证各进程/线程在访问共享资源或在关键计算阶段时保持一致
  3. 安排进程/线程之间的通信:在任务执行过程中,满足数据交换、状态更新和协调要求

方式

  • 动态线程:主线程根据实际任务需求在运行时动态创建工作线程
  • 静态线程:主线程会派生出固定数量的线程,在整个程序生命周期中重复利用这些线程来处理各项任务
  • 远程过程调用:存根将调用的参数进行序列化,封装成网络消息,从而实现节点间的通信

3.2 输入输出

  1. 在分布式内存程序中,只有进程 0 能够访问 stdin;在共享内存程序中,只有主进程能够访问 stdin
  2. 在分布式内存和共享内存系统中,所有进程和线程都可以访问 stdout 和 stderr
  3. 大多数情况下,为了避免输出混乱,只会由一个进程或线程负责将最终结果输出到 stdout
  4. 在调试的时候,为了了解每个进程或线程的内部状态和行为,可能会允许所有进程/线程向 stdout 输出调试信息,但是需要注明注明进程号或线程标识符
  5. 为了避免文件访问冲突和数据竞争,只有一个进程或线程会尝试访问一个特定的文件

4. 并行性能

加速比(Speedup):是串行执行时间与并行执行时间的比值,S(p)=T串行T并行S(p) = \frac{T_{\text{串行}}}{T_{\text{并行}}},其中 p 是核的数量

不可能获得线性加速比即 S 不可能等于 p

效率 (Efficiency):反映了并行系统中每个处理单元在加速比中所发挥的贡献,E(p)=S(p)p=T串行p×T并行E(p) = \frac{S(p)}{p} = \frac{T_{\text{串行}}}{p \times T_{\text{并行}}}

效率较低表明并行化过程中存在较多额外开销或者负载不均

问题规模越大,加速比和效率增加:随着问题规模变大,额外开销时间比串行执行时间增长得慢,线程需要花费更多精力做任务,而协调进程/线程到工作的时间就相对变少了

阿姆达尔定律:除非一个串行程序全部都并行化,否则不论有多少核,并行度有多高,串行部分都会成为性能提升的瓶颈,所产生的加速比具有上限,即如果只有 F% 实现了理想并行化,那么最大加速比为Smax=11FS_{\max} = \frac{1}{1-F}

可扩展性:程序在增加进程或线程数的同时,如果问题规模也按相同比例增长,程序的效率或性能提升能够保持稳定不变

5. Foster 方法

  1. 划分(Decomposition):确定任务之间的依赖关系,找出可以独立并行执行的任务单元,将要执行的指令或数据按照计算部分拆分为多个小任务
  2. 通信(Communication):确定上一步所识别的任务之间需要执行哪些通信
  3. 聚合(Aggregation):将第一步所确定的任务与通信结合成一个聚合任务
  4. 分配(Assignment):将上一步聚合好的任务分配到进程/线程中