在这一章我们会学习到和并行计算相关的一些基础内容,包括但不限于并行计算架构、并行计算模型以及并行计算的相关算法和工具。学起来!
计算机架构分类方法
Flynn在1966年提出了一个经典的分类方法,根据指令和数据两个未读,对多处理器计算机架构进行区分,大致可以分为四个部分。 这页 PPT 介绍了 Flynn 的经典分类法(Flynn’s Classical Taxonomy),该分类法自 1966 年提出,依据 指令(Instruction) 和 数据(Data) 两个独立维度对多处理器计算机架构进行区分,具体如下:
| 分类 | 全称 | 含义与特点 |
|---|---|---|
| SISD | Single Instruction, Single Data(单指令流单数据流) | 传统顺序执行架构,一次处理一条指令和一个数据,如早期单核处理器,无并行性。 |
| SIMD | Single Instruction, Multiple Data(单指令流多数据流) | 一条指令同时作用于多个数据,适合并行处理相似数据,如向量处理器、GPU 部分操作(图形渲染中统一处理像素)。 |
| MISD | Multiple Instruction, Single Data(多指令流单数据流) | 多条指令作用于同一数据,理论概念性强,实际应用较少。 |
| MIMD | Multiple Instruction, Multiple Data(多指令流多数据流) | 多个指令流处理多个数据流,如多核处理器或多处理器系统,支持多任务并行(如服务器处理不同客户端请求)。 |
这种分类方法可以清晰地界定计算机架构的并行处理模式,下面我们逐一了解一下。
SISD
单指令流单数据流架构(Single Instruction,Single Data)是一种串行的计算机架构,在任何一个时钟周期内,CPU仅仅处理一条指令,同时在任何一个时钟周期内,仅使用一个数据流作为输入。
SISD具有良好的确定性执行的特定,指令可以按照顺序逐条执行,同样,结果是可以预测的。从计算机诞生开始,这种方法便被普遍接受,时至今日依旧是较为普遍的计算机形式。大多数人的个人电脑PC、单CPU工作站仍然采用这种方式。
我们举一个SISD的例子:
下图展示了操作随时间的顺序执行过程,先加载数据A和B,接着进行计算C,并对C进行存储,之后在计算A并进行存储,整个过程并无并行操作,严格按照顺序进行,直观体现了SISD架构的串行执行特性。
SIMD
单指令流多数据流架构(Single Instruction,Multiple Data)是一种并行计算机架构,在单个时钟周期内,所有处理单元(Processing Element,PE)均执行相同的指令,每个处理单元可对不同的数据元素进行操作。
这种架构通常包括一个指令分配器、一个高带宽的内部网络以及一个由大量小容量指令单元组成的阵列,并且具有同步且确定性执行的特点。
SIMD的起点足够纯净,架构师将现有的64位寄存器和ALU分为许多8位、16位、32位的块,然后对其进行并行计算。操作码提供数据宽度和操作。 但是自从1978年以来,IA-32指令集已经从80条增加到大约1400条,主要就是由SIMD推动的。
因此x86和AMD的规范手册非常庞大,而RISC-V设计人员使用了矢量指令而不是SIMD进行了扩展,至于其中有什么区别,这一个帖子大家可以看一下,有很多有意思的想法——深入浅出RISC-V的V向量扩展
MISD
多指令流单数据流架构(Multiple Instruction,Single Data),实际上这类并行计算机很少,但仍然会有一些,例如多个频率滤波器对单个信号流进行处理,不同的滤波器可以应用不同的指令,但是都作用在同一个信号流上。
同时在加密算法中,也可以将多种加密算法同时绑定到单个数据流上。
总的来说,应用场景不是很广泛。
MIMD
多指令流多数据流(Multiple Instruction,Multiple Data)。MIMD是目前最常见的并行计算机类型,每个处理器可以执行不同的指令流,各处理器能够根据任务需求独立执行不同操作。并且每个处理器可处理不同的数据流,支持对多样数据的并行处理。
单程序多数据(Single Program Multiple Data,SPMD),即多个处理器运行同一程序的不同部分,处理不同数据,就是MIMD的一种典型应用模式。
这种执行方式十分灵活,既可以同步(各处理器按照同一节奏执行),也可以是异步的;既可以是确定性的(相同输入必产生相同输出)。也可以是非确定的。
当前的各种大规模并行系统、分布式计算环境、多处理器SMP计算机都体现了MIMD在不同规模计算系统的普及。
上图展示了MIMD架构的基本组成,有多个处理单元PE,以及控制单元通过互联网络连接,各个处理单元可以独立执行指令,处理数据,直观呈现了MIMD的并行处理机制。
SIMD VS MIMD
既然SIMD和MIMD都是并行,那么他们之间的差异有哪些?
对于SIMD来说,它的硬件需求更少,仅仅需要一个全局控制单元和一个程序副本,硬件复杂度较低。并且它的专用性更强一些,会依赖专门的硬件架构和应用特性,适合于特定的并行计算(同类型数据),对于不规则问题的支持性较差一些,同时当计算设计条件判断时,部分处理单元可能会闲置,导致资源浪费。
而MIMD呢?首先便可以进行分布式存储和操作,每个处理器都可以独立存储程序和操作系统,具备更强的自主性。并且可以使用廉价的现成组件构建,成本更低,更易实现。同时支持各种数据结构和不规则的任务,适用性更广一些,并且无需复杂的专用架构设计,在短时间便可以实现有效的部署。
并行计算平台的二分法
基于并行计算平台的分类,可以包括逻辑组织和物理组织两个层面。
逻辑组织(程序员视角)
从程序员角度来看,控制结构和通信模型是必须要考虑的两个方面。
- 控制结构。指的是表达并行任务的方式。例如多线程、多进程或者其他并行编程模型来定义任务的并行执行逻辑。
- 通信模型。在并行任务计算的过程中,需要在任务之间进行交互,通信模型指的就是这种任务间的交互机制。比如,任务间可以通过共享内存直接交换数据,或者通过消息传递来实现通信等等。
物理组织(硬件视角)
在硬件看来,同样分为两个方面架构和互联网络。
- 架构涉及硬件的整体设计,比如单核、多核、众核结构,或者采用对称多处理SMP以及非均匀内存访问NUMA等架构模式。
- 互联网络则是指定硬件组件之间的连接方式,例如总线、交叉开关、环形网络、网格网络等等,这些不同的连接方式决定了数据在不同硬件单元(处理器、内存模块)的传输路径和效率。
并行任务的粒度级别
并行任务可以在不同的粒度级别上进行指定。
一个极端是直接将一个程序做为并行任务,机器同时可以运行多个独立的应用程序(这个和多进程很像); 另一个极端是,程序内的单个指令(硬件指令的并行)。
那么在这两个极端之间,就会存在不同粒度的并行任务模型(循环并行、函数并行等等)。
我们可以看一下这个例子:
for (i = 0; i < 1000; i++)
c[i] = a[i] + b[i]
在这个循环中,所有的迭代(无论i取某个值),他们都彼此独立(第i次的循环计算并不依赖于其他循环的结果)。这种独立性使得每个迭代循环都可以作为并行任务来执行,比如分配给不同的处理单元,同时计算c[i],这就可以体现出在循环层面利用并行性。
共享地址空间通信模型
在上述我们提到的并行过程中,通信在任务执行中占据很重要的一个部分,我们的电脑一般都会划分一块单独的公共数据空间,在这块空间中,所有的处理器都可以访问到公共空间数据,这实际上也是共享地址空间的核心——方便处理器之间进行交互。
如果支持单程序多数据(SPMD)编程,则此类平台对应的就是多处理器系统,即多个处理器运行同一个程序的不同部分,处理不同的数据。
在内存上便可以区分为Local内存和Global内存。
有了内存之后,我们还需要对内存进行访问,这访问又有什么门道吗? 你别说,还真有!
这两个词
- 均匀内存访问(UMA、Uniform Memory Access)
- 非均匀访问(NUMA,Non-uniform Memory Access)。
我们来品鉴一下这三张图。 最左侧是一个没有缓存的均匀内存访问,我们可以看到处理器通过互联网络连接到内存,所有处理器访问系统中的任何内存时间都相同,体现出了它的均匀特性。
中间的是一个带缓存的UMA架构,每一个处理器都配备了各自的缓存,并且通过互联网络连接到内存,仍然能够保证UMA的访问特性,同时缓存又可以加速数据访问,提升一定的性能。
最右边的呢就是NUMA,每一个处理器都有本地内存,各个处理器之间通过互联网络连接起来,对于某一个处理器来说,访问Local和访问Others所花费的时间肯定不同,这就体现出了它的“非均匀”特性,这种通常适合大规模多处理器系统。
UMA和NUMA区别在哪?
既然提到了UMA和NUMA,我们就不能想当然的,奥,有这两个概念,而是需要是思考一下,为什么会把这两种访问方式区分开?
首先,NUMA的底层肯定更加优先本地内存,举个栗子。假设我们现在有一个百度网盘,偶尔会下载一些迷人的电影视频,但是当我们想要访问的时候,就需要忍受他漫长的等待时间,但是放在本地呢?点开就开干!简单来说,NUMA 架构下本地内存访问速度更快,这是因为目前大部分机器作为单个完整的节点,都会配备具有本地局部内存。
UMA就更加类似于去电影院,大家都统一去看,对谁都公平。
相比之下,UMA 就类似于大家统一去电影院观看电影,对每个观众来说都公平对待。
一谈到内存,就不可避免地会涉及到诸多问题,如共享数据、读写问题(包括互斥、锁)、数据竞争、缓存一致性等等。
还有一些集群仅仅提供地址映射,但并不进行访问协调。这种集群模型被称为非缓存一致性的共享地址空间,其在缓存管理和数据一致性维护方面与强一致性模型的机器存在显著差异。
全局内存空间
对于并发程序来说,内存是一个很复杂的问题,而全局内存空间可以一定程度上解决这个问题。 全局空间最好的便是对内存地址空间进行统一编址,编程会更加容易,并且通常来说内存对于程序员是不可见的,程序员并不需要显式地管理内存,从只读交互(Read-Only interactions)来说,与串行程序的内存访问方式相同,这样会减少程序员的负担。
在只读过程中,问题很简单, 大家可以并发读,但是一旦涉及到写,就需要进行读写交互,更需要处理并发访问的互斥问题,例如使用锁或者其他机制来保持内存的一致性和数据完整性。
从编程角度来说,可以支持多种编程范式,例如线程(POSIX和Windows NT),或者指令(OpenMP)。
不管怎么说,全局内存空间可以简化并行编程,并且提高程序的可读性。
共享地址空间的缓存
任务
在共享地址空间中,主要有两个任务:
-
地址转换机制
通常来说,我们内存中的地址并不是直接使用物理地址,而是一套虚拟地址,这套机制能够让程序通过虚拟地址访问内存,而系统负责将虚拟地址转换为物理地址,从而找到实际内存的位置。 -
缓存一致性(Cache coherence)
缓存一致性能够确保多个缓存副本之间有明确的语义,这在共享内存系统中尤为重要,因为多个处理器可能会缓存同一个内存位置的副本。
为了支持这个功能,需要进行硬件支持和软件支持。硬件层面需要提供缓存一致性机制,例如使用缓存一致性协议(MESI协议),来自动管理缓存之间的数据一致性。还需要软件支持,例如程序员通过显式的get和put操作来管理数据的获取和更新。这种情况下程序员需要手动确保数据的一致性。
缓存一致性协议可以看一下这个博客CPU缓存一致性与MESI协议,真的一致吗?
思考
Shared-Address-Space和Shared Memory Machines有什么区别?
Shared-Address-Space是一种编程抽象,它提供了一种逻辑上的统一地址空间,使得程序的不同部分(多个线程或者进程)可以使用相同的地址来访问共享数据。并且是一种软件层面上的抽象,目的是为了简化编程模型,让程序员更容易地编写共享数据访问的代码。
而Shared-Memory Machines是一种物理机器属性,它指的是物理硬件架构,其中多个处理器或者核心访问同一块物理内存。在这种架构下,所有处理器都可以直接访问同一块物理内存,数据共享是通过物理总线或者互联网络来实现的。
使用物理分布式内存实现共享地址空间的可能性
在某些计算机系统中,内存可能是物理上分布的,即每个处理器节点都有本地的内存。但是可以通过特定的硬件和软件机制,可以在这种分布式内存系统上构建一个共享地址空间的抽象。
可以使用硬件机制(缓存一致性协议、目录协议等等)来协调不同节点间的内存访问;也需要软件层面来支持,比如通过消息传递或者远程内存访问(RMA)等机制来实现数据的一致性。
Logic View 和 Physical Organization的区别
逻辑视图,尤其是共享地址空间,这是一种编程抽象,允许不同的线程,不同的进程共享使用同一块空间,逻辑上,所有的线程或者进程都会使用同一套地址空间来访问数据。而非共享地址空间呢?每个线程或者进程都会有自己的独立地址空间,那么在访问其他进程或者线程时,都需要显式的通信机制。
在物理层面,共享内存计算机会在物理上使用同一块内存,这种称为“统一内存访问”(UMA)系统,对所有的处理器对内存的访问时间都是相同的;而分布式内存计算机,在物理上每个处理器节点都有自己的独立内存,如果提供统一共享地址空间的抽象,则被称为“非统一内存访问”(NUMA)系统,在NUMA中,访问本地和远程的时间是不同的。
消息传递平台
从逻辑角度来看,假设有p个节点,每个节点都有自己的独立地址空间,这意味着每个节点的内存读及其他节点来说是不可见的,例如集群工作站或者非共享地址空间的多计算机。
在这个基础上,各个机器之间就需要进行数据交互、同步和工作的转移以及交接。
在通信的基础上,我们有两个最简单的操作:send和receive。那我们的编程范式就是基于send和receive来进行通信和同步。
在这里,我们最常见的消息传递接口包括:
- MPI(Message Passing Interface):消息传递接口,广泛应用于并行计算。
- PVM(Parallel Virtual Machine):并行虚拟机,将异构计算机集群视为单一的并行计算资源。
如图所示,两台机器上,每台机器都会运行一个任务,每个任务都有自己的数据,数据通过网络在两个任务之间进行传递。
分布式共享内存DSM
分布式共享内存(Distributed Shared Memory,DSM)或者共享虚拟内存(Shared Virtual Memory,SVM)是一种在分布式系统中提供共享内存抽象的基础,通过DSM,多个物理上分布的节点可以共享在一个虚拟地址空间,使得编程模型更加简洁,类似于共享内存编程。
在访问内存的方式中,主要有两种:基于Page的访问控制和基于Object的访问控制。
Page-Based Access Control
基于页的访问中,会借助OS的虚拟内存管理机制,将物理内存分布式的节点整合成一个统一的虚拟地址空间,将主内存管理视为虚拟地址内存的一个全关联缓存,有助于高效管理和访问分布在不同节点的内存页面。
当页面发生故障时,通过内嵌的一致性协议来确保数据的一致性和完整性,处理Page的获取和更新。
Object-Based Access Control
基于对象的访问中,提供了更高的灵活性,允许程序员以对象为单位进行数据访问阿和同步,这样避免了虚假的共享,即不同线程对不同数据的访问不会因为数据位于同一缓存行或者内存page而产生不必要的开销。
我们来看一下DSM的发展历程,它在不同的阶段有不同的代表性系统。
1. 顺序一致性模型(Sequential Consistency Model)
这是一种严格的内存一致性模型,确保所有处理器的访问操作都按照全局统一的顺序执行,适合于但CPU的工作站集群。
早期的DSM,例如lvy,采用顺序一致性模型,为工作站集群提供共享内存抽象。
放宽一致性模型(Relaxed Consistency Model)
这种模型保证在数据一致性的前提下,允许一定的灵活性,同样适用于单CPU工作站集群,例如TreadMarks,是一个著名的DSM系统,采用放宽一致性模型,旨在提高系统的性能和可扩展性。
放宽一致性模型与多线程(Relaxed Consistency Model and Multi-threading
在之前的基础上,结合了多线程技术,这种更加适合于多处理器计算机网络,可以支持更加复杂的并行环境,代表系统有Brazos和Strings。