第2章 并行硬件与软件 摘录

2.1 背景知识

2.1.1 冯-诺伊曼结构

”经典“的 冯-诺伊曼结构包括主存、中央处理单元(Central Processing Unit, CPU)处理器或核,以及主存和CPU之间的互连结构。

主存中每个区域都可以储存指令数据。每个区域都有一个地址,可以通过这个地址来访问相应的区域及区域中存储的数据和指令。

中央处理单元分为控制单元和算术逻辑单元(Arithmetic Logic Unit, ALU)。控制单元负责决定应该执行程序中那些指令,而ALU负责执行指令。CPU中的数据和程序执行时的状态信息存储在特殊的快速存储介质中,即寄存器。控制单元有一个特殊的寄存器,叫做程序计数器,用来存放下一条指令的地址。

指令和数据通过CPU和主存之间的互连结构进行传输。这种互连结构通常是总线,总线中包括一组并行的线以及控制这些线的硬件。

冯-诺伊曼机器一次执行一条指令,每条指令对一个数据进行操作。

当数据或指令从主存传到CPU时,我们称之为数据或指令读取。当数据或指令从CPU传送到主存中,我们称数据或指令的写入。

主存和CPU之间的分离称为冯-诺伊曼瓶颈。

 操作系统:一种用来管理计算机的软件和硬件资源的主要软件。

当用户运行一个程序时,操作系统创建一个进程。进程是运行着的程序的一个实例。

一个进程包括以下实体:
        1.可执行的机器语言程序。
        2.一块内存空间,包括可执行代码,一个用来跟踪执行函数的调用栈、一个堆,以及一些其他的内存区域。
        3.操作系统分配给进程的资源描述符,如文件描述符。
        4.安全信息,例如阐述进程能够访问那些硬件和软件的信息。
        5.进程状态信息,例如进程是否就绪还是等待某些资源,寄存器内容,以及关于进程存储空间的信息。

在一个多任务操作系统中,如果一个进程需要等待某个资源,例如需要从外部的存储器读取数据,它会阻塞。

线程为程序员提供了一种机制,将程序划分为多个大致独立的任务,当某个任务阻塞时能执行其他任务。此外线程间的切换比进程间切换更快。因为线程相对于进程而言是’轻量级‘的。

线程包含在进程中,所以线程能使用相同的可执行代码,共享相同的内存和相同的I/O设备。

实际上,当两个线程共属于一个进程时,它们共享进程的大部分资源。它们之间最大区别在于各自需要一个私有的程序计数器和函数调用栈,使它们能够独立运行。

当一个线程开始工作时,它从进程中派生(fork)出来;当一个线程结束,它合并(join)到进程中。

2.2 对冯-诺伊曼模型的改进

三种改进措施:缓存(caching)、虚拟存储器(虚拟内存)、低层次运行。

2.2.1 Cache基础知识

缓存将互连通路加宽,使得一次内存访问能够传送更多的数据或者指令。而且不在是将所有数据和指令存储在主存,可以将部分数据块或者代码块存储在一个靠近CPU寄存器的特殊存储器里。

有了Cache之后,一个很明显的问题是什么样的数据和指令能够存储在Cache中。通用的准则是程序接下来可能会用到的指令和数据与最近访问过的指令和数据在物理上是临近存放的。

程序访问完一个存储区域往往会访问接下来的区域,这个原理称为局部性。在访问完一个内存区域(指令或者数据),程序会在不久的将来(时间局部性)访问临近的区域(空间局部性)。

为了运用局部性原理,系统使用更宽的互连结构来访问数据和指令。也就是,一次内存访问能存取一整块代码和数据,而不是单条指令和单条数据。这些块称为高速缓存块或者高速缓存行。

实际上,Cache分为不同的层,L1最小但最快, L2, L3更大但相对较慢。

Cache通常存储速度较慢的存储器中的信息副本,可以认为底层Cache(更快,更小)是高层Cache的Cache。

但是,有些多层Cache不会复制已经在其他层Cache中存在的消息。L1 Cache中的变量不会存储在其他层Cache,但会存储在主存中。

当CPU需要访问指令或者数据时,它会沿着Cache的层次结构向下查询,首先查询L1 Cache, 接着L2 Cache,以此类推。最后,如果Cache中没有所需要的信息,就会访问主存。

当向Cache查询信息时,如果Cache中有信息,则称为Cache命中。如果信息不存在,则称Cache缺失。命中和缺失是相对Cache层而言的。

当CPU尝试读取数据或者指令时,如果发生Cache缺失,那么就会从主存中读出包含所需信息的整个高速缓存块。这时CPU就会阻塞,因为它需要等待速度较慢的主存:处理器可以停止执行当前程序的指令,直到从主存中取出所需的数据或者指令。

当CPU向Cache中写数据时,Cache中的值与主存中的值会不一致(inconsistent)。有两种办法来解决不一致性问题。在写直达(write-thgrough)Cache中,当CPU向Cache写数据时,高速缓存行会立即写入主存中。在写回(write-back)Cache中,数据不是立即更新到主存中,而是将发生数据更新的高速缓存行标记称脏(dirty)。当发生高速缓存行替换时,标记为脏的高速缓存行被写入主存中。

2.2.2 Cache映射

在Cache设计中,另一个问题是高速缓存行应该存储在什么位置。一个极端是全相联(full associative) Cache,每个高速缓存行能够放置在Cache中的任意位置。另一个极端是直接映射(directed mapped) Cache,每个高速缓存行在Cache中只有唯一的位置。处于两种极端中间的方案是n路组相连(n-way set associated)。在n路相连的Cache中,每个高速缓存行都能放置在Cache中n个不同区域位置中的一个。

当内存中的行(多余一行)能被映射到Cache中的多个不同位置(全相联和n路组相连)时,需要决定替换或者驱逐Cache哪一行。最常用的替换方案是最近最少使用(least recently used)。Cache记录各个块被访问的次数,替换最近访问次数最少的块。

2.2.3 Cache一个实例

C语言是’行主序‘来存储二维数组。尽管二维数组看上去是一个矩形块,但内存是一个巨大的一维数组。在行主序存储模式下,先存储二维数组的第一行,接着第二行,依次类推。

2.2.4  虚拟存储器

如果运行一个大型程序或者程序需要访问大型数据集,那么所有指令或者数据在主存中放不下。这在多任务操作系统中时常发生;为保证程序间切换并且造成一种多个程序能够同时运行的错觉,下一个时间片运行所需的指令和数据必须在主存中。因此,在多任务操作系统中,即使主存非常大,许多运行中的程序必须共享有限的主存。此外,这种共享必须保证每个程序的数据和指令能被保护,不会被其他程序访问。

利用虚拟存储器(或虚拟内存),使得主存可以作为辅存的缓存。它通过在主存中存放当前执行程序所需要用到的部分,来利用时间和空间局部性;那些暂时用不到的部分存储在辅存的块中,称为交换空间(swap space)中,与CPU Cache类似,虚拟存储器也是相对数据块和指令快进行操作。这些块通常称为页(page)。因为访问辅存比主存要慢成千上万倍,所以页通常比较大。大多数采用固定大小的页,从4~16kb不等。

如果在编译程序时直接给页指定物理内存地址,程序中的每一页也都必须指定一块内存来存放,在多任务操作系统中,就会导致多个程序要使用相同的内存块。为避免这个问题,在编译程式时,给程序的页赋予虚拟页号。当程序运行时,创建一张将虚拟页号映射成物理地址的表。

使用页表的缺点是,会使访问主存区域的时间加倍。

2.2.5 指令级并行

指令级并行(instruction-level parallelism, ILP)通过让多个处理器或者功能单元同时执行指令来提高处理器的性能。有2种主要方法:流水线和多发射。
        1.流水线是指将功能单元分阶段安排;
        2.多发射是指让多条指令同时启动;

流水线通过将功能分成多个单独的硬件或者功能单元,并把它们按顺序串接来提高性能。

比如求两个数组之和:

时间操作操作数一操作数二结果
0取操作数9.8*10^46.5*10^3
1比较操作数9.8*10^46.5*10^3
2移位操作数9.8*10^40.65*10^4
3相加9.8*10^40.65*10^410.45*10^4
4规格化结果9.8*10^40.65*10^41.045*10^5
5舍入结果9.8*10^40.65*10^41.05*10^5
6存储结果9.8*10^40.65*10^41.05*10^5
时间比较移位规格化四舍五入存储
0操作数0
1操作数1操作数0
2操作数21操作数0
3操作数321操作数0
4操作数4321操作数0
5操作数54321操作数0
6操作数654321操作数0

总的来说,k个阶段的流水线不可能达到k倍的性能提高。因为各种功能单元的运行时间不同,每个阶段的有效运行时间取决于最慢的功能单元。此外,有些延迟(例如等待操作数)也会造成流水线阻塞。

而多发射处理器通过复制功能单元来同时执行程序中的不同指令。第一个加法器计算第一个数组元素,第二个加法器计算第二个数组元素...

如果功能单元是在编译时调度的,则称该多发射系统使用静态多发射;如果是在运行时调度的,则称该多发射系统使用动态多发射。一个支持动态多发射的处理器称为超标量。

为了能够利用多发射,系统必须找出能够同时执行的指令。其中一个最重要的技术是预测(speculation)。如果预测工作由编译器来做,那么它通常在代码中嵌入测试语句来验证预测的正确性,如果预测错误,就会执行修正操作。假如硬件做预测操作,处理器一般会将预测执行的结果缓存在一个缓冲器中。如果预测正确,缓冲器中的内容会传递给寄存器或者内存;如果预测错误,则缓冲器中的内容被丢弃,指令重新执行。

2.2.6 硬件多线程

指令级并行是很难利用的,因为程序中有许多部分之间存在依赖关系。比如计算斐波那契数。

线程级并行(Thread-Level Parallelism, TLP) 尝试通过不同线程来提供并行性。与ILP相比,TLP提供的是粗粒度的并行性,即同时执行的程序基本单元比细粒度的程序单元(单条指令)更大或者更粗。

硬件多线程(hardware multithreading)为系统提供一种机制,使得当前执行的任务被阻塞时,系统能够继续其他有用的工作。系统必须实现线程间的快速切换。进程间的切换时间是执行指令时间的数千倍。

细粒度(fine-grained)多线程中,处理器在每条指令执行完后切换线程,从而跳过被阻塞的线程。尽管这种方法能够避免因为阻塞而导致机器时间的浪费,但它的缺点是,执行很长一段指令的线程在执行每条指令的时候都需要等待。粗粒度(coarse-grained)多线程为了避免这个问题,只切换那些需要等待较长时间才能完成操作(如从主存中加载)而被阻塞的线程。这种机制的优点是,不需要线程间的立即切换。但是,处理器还是可能在短阻塞时空闲,线程间的切换会导致延迟。

同步多线程(Simultaneous Multithreading, SMT)是细粒度多线程的变种。它通过允许多个线程同时使用多个功能单元来利用超标量处理器的性能。如果我们指定’优先’线程,那么能在一定程度上减轻线程减速的问题。优先线程是指有多条指令就绪的线程。

2.3 并行硬件

2.3.1 SIMD系统

Flynn分类法按照它能够同时管理的指令流数目和数据流数目来对系统分类。
        1.SISD:一次执行一条指令,一次存取一个数据项;
        2.SIMD:通过对多个数据执行相同的指令从而实现多个数据流上的操作。

一个抽象的SIMD系统可以认为有一个控制单元和多个ALU。一条指令从控制单元广播到多个ALU,每个ALU或者在当前数据上执行指令或者处于空闲状态。注意在经典的SIMD系统中,ALU必须同步操作,即在下一条指令开始之前,每个ALU必须等待广播。此外,ALU没有指令存储器,所以ALU不能通过存储指令来延迟执行指令。

SIMD适合处理大型数组简单循环实现并行化。通过将数据分配给多个处理器,然后让各个处理器使用相同的指令来操作数据子集实现并行化。这种并行称为并行化。

唯一广泛生产的SIMD系统是向量处理器。近年来,GPU和台式机CPU利用了SIMD计算方面知识。

向量处理器的重要特点还是数组或者数据向量进行操作,而传统的CPU是对单独的数据元素或者标量进行操作。

向量寄存器能够存储由多个操作数组成的向量,长度由系统决定,从4到128个64位元素不等。

向量化功能单元对向量中的每个元素需要做同样的操作。操作需要应用到两个向量中对应元素对上。因此,向量操作是SIMD。

向量指令:在向量上操作而不是在标量上操作的指令。

交叉存储器(bank?)。内存系统由多个内存‘体’组成,每个内存体能够独立访问。在访问完一个内存体后,再次访问它之前需要一个时间延迟。

步长式存储器访问和硬件的散射/聚集。在步长式存储器访问中,程序能够访问向量中固定间隔的元素。例如能够以跨度4访问第一个,第五个,第九个元素等。散射/聚集式对无规律间隔的数据进行读(聚集)和写(散射)。例如访问第一个,第二个,第四个,第八个元素等。典型的向量系统通过提供特殊的硬件来加速步长式存储器访问和散射/聚集操作。

向量系统有很高的内存带宽,每个加载的数据都会使用,不像Cache的系统不能完全利用高速缓存行中每个元素。但是,它不能处理不规则的数据结构和其他的并行结构,这限制它的可扩展性。

可扩展性是指能够处理更大问题的能力。

GPU可以通过使用SIMD并行来优化性能。现在所有的GPU都是用SIMD并行,通过在每个GPU处理核中引入大量的ALU来获取的。

GPU严重依赖硬件多线程。GPU的缺点是需要处理大量的数据的线程来维持ALU的忙碌,可能在小问题的处理上性能相对较差。

需要强调的是,GPU不是纯粹的SIMD系统。尽管在一个给定核上的ALU使用SIMD并行,但现代的GPU有几十个核,每个核都能独立执行指令流。

2.3.2 MIMD系统

多指令多数据流(Multiple Instruction Multiple Data, MIMD)系统同时支持多个指令流在多个数据流上操作。因此,MIMD系统通常包括一组完全独立的处理单元或者核,每个处理单元或者自己的核都有自己的控制单元核ALU。此外,不同于SIMD系统,MIMD系统通常是异步的,即各个处理器能够按照它们自己的节奏运行。在许多MIMD系统中,没有全局时钟,两个不同的处理器上的系统时间是没有联系的。实际上,除非程序员强制同步,即使处理器在执行相同顺序的指令时,在任意时刻它们都可能执行不同的语句。

MIMD系统有两个类型:对称多处理系统和分布式内存系统。

在对称多处理系统中,一组处理器通过互连网络(internection network)与内存系统相互连接,每个处理器能够访问每个内存区域。处理器通过访问共享的数据结构来隐士地通信。

在分布式内存系统中,每个处理器有私有内存空间,处理器-内存对之间通过互连网络相互通信。处理器之间是通过发送消息或者使用特殊函数来访问其他处理器地内存,从而进行显示地通信。

在拥有多芯共享内存系统中,互连网络可以将所有的处理器直接连到主存,或者也可以将每个处理器直接连到一块内存,通过处理器中内置的特殊硬件使得各个处理器可以访问内存中的其他块。

                                                                        UMA

NUMA

在互连网络将所有处理器直接连接到内存的系统中,每个核访问内存中任何一个区域的时间都相同。(一致内存访问, Uniform Memory Access, UMA)

在互连网络将每个处理器直接连到一块内存的系统中,访问与核直接相连的那块内存区域比访问其他内存区域要块很多。(非一致内存访问, Nonuniform Memory Access, NUMA)

UMA系统通常比较容易编程,因为程序员不用担心不同的内存区域的不同访问时间。在NUMA系统中,对与核直接连接的内存区域的访问速度较快,失去了易于编程的优点,但NUMA系统能够比UMA系统使用更大容量的内存。

最广泛使用的分布式内存系统称为集群(clusters)。它们由一组商品化系统(PC)组成,通过商品化网络连接(以太网)。实际上,这些系统中的节点(由通信网络相互连接的独立计算单元),通常都是有一个或者多个芯片的共享内存系统。

网络提供一种基础架构,使地理上分布的计算机大型网络转换成一个分布式内存系统。

2.3.3 互连网络

互联网络(interconnection network) 在分布式内存系统和对称多处理器系统中都扮演了一个决定性的角色。即使处理器和内存无比强大,但一个缓慢的互连网络会严重降低除简单并行程序外所有程序的整体性能。

在共享内存系统中,目前最常见的两种互连网络是总线(Bus)和交叉开关矩阵(crossbar).

总线是由一组并行通信线和控制对总线访问的硬件组成,核心特征是连接到总线上的设备共享通信线。总线具有低成本和灵活性的优点,多个设备以较小的额外开销连接到总线上。但是,因为通信线是共享的,因此随着连接到总线设备的增多,争夺总线的概率增大,总线的预期性能会下降。随着共享内存系统规模的增大,总线会迅速被交换互连网络所取代。

交换互连网络使用交换器(switch)来控制相互连接设备之间的数据传递。线表示双向通信链路,方块表示核或者内存模块,圆圈表示交换器。

无论数据是在主存和Cache之间传递、在Cache和寄存器之间,在磁盘和内存之间、还是在两个分布式内存系统或者混合系统的节点之间传送,我们关心数据到达目的地需要花多少时间。经常用来衡量互连网络性能指标:延迟(latency)和带宽(bandwidth)。

延迟是指从发送源开始传送数据到目的地开始接受数据之间的时间。

带宽是指目的地在开始接收数据后接受数据的速度。

如果一个互连网络的延迟是l秒,带宽是b字节每秒,则传输一个n字节的消息需要花费的时间是:

                                                                l + n/b 

在分布式系统中,延迟可能是指在发送端收集信息的时间,将不同部分组装起来的时间,以及在接收端消息拆卸的时间,从消息中抽取原始数据的时间以及在目的地存储的时间的总和。

2.3.4 Cache一致性

CPU Cache是由系统硬件来管理的,程序员对它不能进行直接的控制。这对共享内存系统带来很多重大的影响。程序员对Cache没有直接控制。

在多核系统中,各个核存储相同变量的副本,当一个处理器更新Cache中该副本时,其他处理器应该知道该变量已更新,即其他处理器中Cache的副本也应该更新。这称为Cache一致性问题。

有两种方法来保证Cache一致性:监听Cache一致性协议和基于目录的Cache一致性协议。

监听协议的想法来自于基于总线的系统:当多个核共享总线时,总线上传递的信号都能被连接到总线的所有核‘看’到。因此,当核0跟新它Cache中x的副本时,如果它也将这个跟新信息的整个行在总线上广播,并且假如核1正在监听总线,那么它会知道x已经更新了,并将自己Cache中的x的副本标记为非法的。

关于监听,有几点必须要考虑。第一,互连网络不一定必须是总线,只要能够支持从每个处理器广播到其他处理器。第二,监听协议能够在写直达和写回Cache上都能工作。原则上,如果互连网络可以像总线那样被Cache共享,如果是写直达Cache,那么就不需要额外的互连网络开销,因为每个核都能‘监测’写;如果是写回Cache,那么就需要额外的通信,因为对Cache的更新不会立即发送给内存。

不妙的是,在大型网络上,广播是非常昂贵的。监听Cache一致性协议每更新一个变量时就需要一次广播。所以监听Cache一致性协议是不可扩展的,因为对大型系统,它会导致性能的下降。

基于目录的Cache一致性协议通过使用一个叫做目录(directory)的数据结构来解决上面的问题。目录存储每个内存行的状态。一般地,这个数据结构是分布式的。每个核-内存对负责一部分存储的目录。这个目录标识局部内存对应高速缓存行的状态。于是,当一个高速缓存行被读入时,如核0的Cache,与这个高速缓存行相对应的目录项就会更新,表示核0有这个行的副本。当一个变量需要更新时,就会查询目录,并将所有包含该变量高速缓存行置为非法。

显然目录需要大量额外的存储空间,但是,当一个Cache变量更新时,只需要更改这个变量所对应的部分目录。

CPU Cache是由硬件来实现的。硬件是对高速缓存行操作的而不是对单独的变量操作。

若系统表现的好像核之间会共享某变量时,称为伪共享。伪共享不会引发错误结果,但是,它能引起过多不必要的访问,降低程序的性能。可以通过在线程或者进程中临时存储数据,再把临时存储的数据更新到共享存储来降低伪共享带来的影响。

2.3.5 共享与分布式存储

在讨论共享与分布式存储时,共享数据结构隐式地协调多个处理器地工作,比显式地发送消息更吸引人。主要的硬件方面问题是互连网络扩展的代价。当向总线增加处理器时,访问总线发生冲突的可能性骤升。所以总线适合处理器数目较少的系统。大型的交叉开关矩阵是非常昂贵的,所以使用大型交叉开关矩阵互连的系统也比较少见。另一方面,分布式互连网络,如超立方体,环面网络相对便宜一些,有成千上万个处理器的分布式系统就是用这种互连网络或者其他构建的。因此分布式系统比较适合于那些需要大量的数据和计算的问题。

2.4 并行软件

通常,在运行共享内存系统时,会启动一个单独的进程,然后派生(fork)出多个线程;在运行分布式内存程序时,我们使用的是多个处理器,所以指的是正在执行任务的进程。

2.4.1 注意事项

最后,关注一下单程序多数据流(Single Program, Multiple Data, SPMD)。SPMD程序不是在每个核上运行不同程序,相反,SPMD程序仅包含一段可执行代码,通过使用条件转移语句,可以让这一段代码在执行时表现得像是在不同的处理器上执行不同的程序。(条件分支)

如果一个程序是通过将任务划分,分给各个进程或者线程来实现并行,则称它是任务并行(task parallel)。

2.4.2 进程或线程的协调

使得每个进程或线程获得大致相等的工作量,称为负载平衡(load balancing)。

将串行程序或者算法转换为并行程序的过程叫做并行化(parallelization)。

如果通过简单地将任务分配给进程/线程来实行并行化,称该程序是易并行的(embarrassingly parallel)。

2.4.3 共享内存

在共享内存系统中,变量可以是共享的(shared)或者私有的(private)。共享变量可以被任何线程读写,而私有变量只能被单个线程访问。线程间的通信时通过共享变量实现的,所以通信是隐式的,而不是显式的。

在许多情况下,共享程序使用的是动态线程。在此范式下,有一个主线程,并在任何时刻都有一组工作线程(可能为空)。主线程通常等待工作请求(例如,通过网络),当一个请求到达时,它派生出一个工作线程来执行该请求。当工作完成任务,就会终止执行再合并到主线程中。此种模式充分利用了系统的资源,因为线程需要的资源只在线程实际运行时使用。

另一种程序运行模式是静态线程。在此范式中,主线程在完成必须的设置后,派生出所有线程,在工作结束前所有的线程都在运行。当所有的线程都合并到主线程后,主线程需要做一个清理工作(释放内存),然后也终止。在资源利用方面,此范式效率不高:如果线程闲置,它的资源(如栈、程序计数器等)不能被释放。但是,线程的派生和合并操作是很耗时的。所以如果所需资源是可用的,静态线程模式比动态线程模式获得更高的性能。

在任何一个MIMD系统中,如果处理器异步执行,那么很可能会引发非确定性。给定输入能产生不同的输出,这种计算称为非确定性。

在共享内存程序中,非确定性是灾难性的,因为它们很容易导致程序错误。

当进程或者线程尝试同时访问一个资源时,这种访问会引发错误,我们经常说程序有竞争条件(race condition),因为线程或者进程处于竞争状态下。即程序的输出依赖于赢得竞争的进程或者线程。

一次只能被一个线程执行的代码称为临界区(critical section),通常是程序员的责任来保证互斥地访问临界区。

保证互斥执行地最常用机制是互斥锁(mutual exclusion clock),或者互斥量(mutex),或者锁(lock)。

互斥量是由硬件支持的一种特殊类型的对象。基本思想是每个临界区由一个锁来保护。在一个线程能够执行临界区中的代码前,它必须通过调用一个互斥量函数来获取互斥量,在执行完临界区代码时,通过调用解锁函数来释放互斥量。

当一个线程‘拥有’锁时,即从调用加锁函数返回但还没有调用解锁函数时,其他线程尝试执行临界区中的代码必须在调用加锁函数时等待。

使用互斥量加强了临界区的串行性。因为在临界区中,一次只有一个线程能执行代码。

还有其他可以替他互斥量的方式。在忙等待(busy-waiting)时,一个线程进入一个循环,这个循环每次测试一个条件。但是忙等待浪费系统资源,因为即使线程在做无用功,执行该线程的核还是会重复的检查是否能进入临界区。

信号量(semaphore)与互斥量类似,尽管它们的行为细节略有不同。对某些类型线程,使用信号量实现同步比互斥量简单。

监视器(Monitor)能够在更高层次提供互斥执行。监视器是一个对象。这个对象的方法,一次只能被一个线程执行。

另一个可能被广泛使用的方法是事务内存(transactional memory)。在数据库管理系统中,事务是系统访问数据库的单位。事务应该要么全部执行,要么都不执行。事务内存背后的基本思想是将共享内存系统中的临界区看作事务。要么一个线程成功地执行整个临界区代码,要么全部回滚,临界代码区重复执行。

在函数调用时不会销毁静态变量。因此,静态变量能够被函数的线程共享,这会引起无法预测和不必要的后果。

线程安全是多线程编程时的一个概念。在拥有共享数据的多线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

2.4.4 分布式内存

对于分布式内存应用程序编程接口而言,它也能在共享内存硬件上使用。只是从逻辑上将共享内存分割成私有的地址空间,给各个线程使用,并使用库函数或者编译器实现所需要的通信。

消息传递的API至少要提供一个发送和一个接收函数。进程之间通过它们的序列号(rank)互相识别。序号范围从0~p-1,其中p是进程的个数。

典型的消息传递API还提供各种‘集体(collective)’通信,如广播(broadcast)。在广播通信中,单个进程传送相同的数据给所有的进程。又例如归约(reduction),在归约函数中,将各个进程计算的结果汇总成一个结果。

消息传递是开发并行的利器。但是它是非常底层的,程序员需要管理很多细节。

单向通信;划分全局地址空间(Partitioned Global Address Space, PGAS)语言提供了一些共享内存程序的机制。

2.4.5 混合系统编程

在类似的多核处理器集群的系统中使用混合编程方法,即在节点上使用共享内存API,而在节点间通信使用分布式内存API,是完全可能的。

2.5 输入和输出

当多个进程能够访问stdout、stdin时,输入的分布和输出的顺序是非确定的。

当并行程序需要输入/输出时,假设并遵循一些规则:
        1.在分布式内存中,只有进程0能够访问stdin。在共享内存程序中,只有主线程或者线程0能够访问stdin。
        2.在分布式内存和共享内存中,所有进程和线程都能访问stdout和stderr。

因为输出到stdout的非确定性结果,大多数情况下,只有一个进程/线程会将结果输出到stdout。但输出调试程序结果是个例外,允许多个进程/线程写stdout。

只有一个进程/线程会尝试访问一个除stdin,stdout,stderr外的文件。所以,每个进程/线程能够打开自己私有的文件进行读写。但是没有两个进程/线程能够打开相同的文件。

调试程序输出在生成输出结果时,应该包括进程/线程的序号或者进程标识符。

2.6 性能

2.6.1 加速比和效率

S:加速比
s:串行
p:并行or核数

效率E等于加速比除核数;

实际上由于多个进程/线程会引入一些代价,共享内存的临界区,需要使用一些互斥机制。分布式内存跨网络传输数据。

2.6.2 Amdahl' law

s:串行时间占比

极限是1/s;

但是,实际情况是Amdahl定律没有考虑问题的规模性。当问题规模增加时,程序中不可并行部分比列却在减小。参考Gustafson定律。该定律有两点假设
        1.问题规模增加后,不能并行执行部分的规模是固定的,换句话说在多处理器上串行执行的时间是固定的。
        2.程序在多处理器上执行的总时间是常数。

2.6.3 可扩展性

如果增加程序所用的进程/线程数,以及在输入规模也以相应增长率增加的情况下,该程序的效率值一直是E,则称该程序是可扩展的。

如果在增加进程/线程的个数时,可以维持固定的效率,却不增加问题的规模,那么程序是强可扩展的(strongly scalable)。

如果在增加进程/线程的个数时,只有以相同倍率增加问题的规模才能维持固定的效率,那么程序是弱可扩展的(weakly scalable)。

2.7 并行程序设计

面对一个串行程序,一般情况下,需要将工作进行拆分,让其分布在各个进程/线程中,使每个进程所获得的工作量大致相同,并且使通信量最小。

Foster方法(PCAM):

P: Partition(划分),给定问题需要分解成子问题,关键在于识别出可以并行的任务。主要划分方案有数据并行、任务并行、模型并行。通过把问题划分为更多小任务来确定潜在的并行资源。数据分解(区域分解)和任务分解(功能分解)。在数据分解中,首先为数据确定一个合适的划分方案,然后安排相应的计算。在任务分解中,首先分解计算流程,然后安排相应的数据。在划分阶段,常常以标识任务数目的最大化为目标(称之为细粒度并行化)。值得注意的是,与任务并行性相比,数据并行性常常粒度更细。

    C: Communication(通信),选定的划分方案决定了进程或线程之间需要的通信量和通信类型。最理想情况是从上一步划分出来的片段(任务/数据)是完全独立的。然而,更常见的情况是两个任务之间必须传输数据,术语上称这些任务之间链接的通道。在通道上,一个任务能够发送消息,另一个任务能够接受消息。后一个任务输入数据依赖前一个任务数据输出,这一依赖过程中需要传输的数据量是确定的。通信模式一般有局部/全局、同步/异步、结构/非结构、静态/动态。根据通道与通信模式,可创建任务依赖图,用节点表示任务,用边表示通信量,用分级表示同步与异步。

    A: Agglomeration(聚集),通信妨碍并行计算。希望通过合并一些小任务变成更大任务,提高在划分阶段和通信阶段设计的粒度。其中一个方法是,使用不同的进程或者线程并行执行大量的小任务,可能会因为通信的额外开销而大大降低效率。为减少这样的额外开销,将几个小任务在同一台机器聚合成一个独立的更大任务可能是有益的。这常常会提高数据的局部性,并且因此减少任务之间数据通信的数量。

    M: Mapping(映射),将上一步通信最小化后的任务分配到处理器上执行。映射过程的目标是:1. 通过把通信频率高的任务分配到同一个处理器以降低处理器之间的通信;2. 通过把任务分配到能并行执行的不同处理器以促进并发;3. 在处理器之间平衡工作负载。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值