
1.6 并行硬件平台
不同的并行计算平台适合不同的并行编程模式,对于某个具体的并行应用来说,如果选对了并行硬件平台,实现后的性能通常会比较好,编程也会简单。下面列出一些常用的并行硬件平台,并说明其适合的编程方式及特点。
1.机群
通过使用网线依据某种拓扑方式将多台微机互连以获取更大的计算能力,这种系统通常称为机群,而每台微机称为节点。目前几乎所有的超级计算机都是机群系统。
通常机群通过TCP/IP协议通信,使用物理网络互连,为了提高通信效率,一些超级计算机也会使用特有的协议和网络硬件。
目前常用于机群通信的网线主要有万兆以太网和InfiniBand网卡,其中万兆以太网的最高速度为1.25GB/s,而InfiniBand可达7GB/s,映射到处理32位浮点数据时的速度,这大约是现在的多核向量处理器速度的千分之一,极易在计算时成为瓶颈。
将多台机器联结在一起的方式称为网络互连,目前流行的互连方式有星形、环形、树形、网格和超立方。由于不同的互连方式可能导致程序运行时信息的路由路径不同,其对数据传输的延迟影响非常大,如网格就适合具有二维局部性的数据传输应用。
由于节点具有独立的处理器和存储器,在机群上编程需要使用显式或隐式的机制指定数据何时需要在不同的节点间传输和接收。在程序运行时,程序需要通过网络互连在各个节点间交换数据,那么数据的传输路径就会影响传输延迟和带宽,因此显式的消息传递编程接口MPI成为这类平台上的标准和首选。
使用MPI在机群系统上编程时,需要提前处理好输入数据和输出数据在节点间的分布,以利用各个节点能提供的带宽。
2.X86多核向量处理器
多核向量处理器指将两个或更多独立单核向量处理器核封装在一个集成电路(IC)芯片中。多核向量处理器既可以执行向量运算,又可以执行线程级并行处理。由于生产商大量生产这种集成多个向量核心的芯片,因此硬件随处可得,近年来越来越受到开发人员的重视。
(1)X86多核
相比超线程,多核是真正的线程级并行设备。多核与超线程技术相结合,可能会进一步提高系统的吞吐量。
多核的每个核心里面具有独立的一级缓存,共享的或独立的二级缓存,有些机器还有独立或共享的三级/四级缓存,所有核心共享内存DRAM。通常第一级缓存是多核处理器的一个核心独享的,而最后一级缓存(Last Level Cache,LLC)是多核处理器的所有核心共享的,大多数多核处理器的中间各层也是独享的。如Intel Core i7处理器具有4~8个核,一些版本支持超线程,其中每个核心具有独立的一级数据缓存和指令缓存、统一的二级缓存,并且所有的核心共享统一的三级缓存。
由于共享LLC,因此多线程或多进程程序在多核处理器上运行时,平均每个进程或线程占用的LLC缓存比使用单线程时要小,这使得某些LLC或内存限制的应用的可扩展性看起来没那么好。
由于多核处理器的每个核心都有独立的一级缓存,有时还有独立的二级缓存,使用多线程/多进程程序时可利用这些每个核心独享的缓存,这是超线性加速(指在多核处理器上获得的性能收益超过核数)的原因之一。
图1-1展示了某个AMD多核处理器的缓存组织结构。
硬件生产商还将多个多核芯片封装在一起,称之为多路,多路之间以一种介于共享和独享之间的方式访问内存。由于多路之间缺乏缓存,因此其通信代价通常不比DRAM低。一些多核也将内存控制器封装进多核之中,直接和内存相连,以提供更高的访存带宽。
多路上还有两个和内存访问相关的概念:UMA(均匀内存访问)和NUMA(非均匀内存访问)。UMA是指多个核心访问内存中的任何一个位置的延迟是一样的,NUMA和UMA相对,核心访问离其近(指访问时要经过的中间节点数量少)的内存其延迟要小。如果程序的局部性很好,应当开启硬件的NUMA支持。

图1-1 多核结构示意(图片来自Internet)
发挥多核处理器多个核心性能的编程方式通常是使用OpenMP和pthread等线程级并行工具,容易产生的性能问题主要是伪共享和负载均衡。
(2)X86向量指令
SSE是X86多核向量处理器支持的向量指令,具有16个长度为128位(16个字节)的向量寄存器,处理器能够同时操作向量寄存器中的16个字节,因此具有更高的带宽和计算性能。AVX将SSE的向量长度延长为256位(32字节),并支持浮点乘加。在不久的将来,Intel会将向量长度增加到512位。由于采用显式的SIMD编程模型,SSE/AVX的使用比较困难,范围比较有限,使用其编程实在是一件痛苦的事情。
MIC是Intel的众核架构,它拥有大约60个X86核,每个核心包括向量单元和标量单元。向量单元包括32个长度为512位(64字节)的向量寄存器,支持16个32位或8个64位数的同时运算。目前MIC的核为按序的,因此其性能优化方法和基于乱序执行的X86处理器核心有很大不同。
为了减小使用SIMD指令的复杂度,Intel寄希望于编译器,实际上Intel的编译器向量化能力非常不错,但是通常手工编写的向量代码性能会更好。在MIC上编程时,软件开发人员的工作由显式使用向量指令转化为改写C代码和增加编译制导语句以让编译器产生更好的向量指令。
要发挥X86向量处理器的向量计算能力,可以使用三种编程方式:
1)使用串行C语言,让编译器产生向量指令,或使用编译制导语句(如OpenMP 4.0)。这种方式最为简单,代码的可移植性通常也最好,但是给予软件开发人员的控制力最差,通常能够发挥的性能也最差。
2)使用Intel规定的内置函数。使用这种方式需要软件开发人员显式地、使用C函数来指定如何向量化操作,使用向量指令来加载数据。使用内置函数时,需要注意哪些内置函数被处理器支持,哪些不被支持(尤其是在开发机和线上机架构不同的情况下)。
3)使用汇编语言。当编译器生成了不够优化或不需要的指令时,就需要使用汇编语言来榨取系统的最后一点性能。使用汇编语言编程相当不便,代码也难以调试,故应当作为不得已的选择。
另外,现代64位CPU还利用SSE指令执行标量浮点运算。
3.GPU+CPU
近年来GPU(Graphics Processing Unit,图形处理器)的晶体管集成度和(向量多核并行)处理能力的发展速度都远远快于CPU(Central Processing Unit,中央处理器),CPU与GPU的融合是芯片技术发展的一种大趋势。Intel和AMD都在其CPU中集成GPU,而NVIDIA和ATI(AMD)则增强其GPU的编程能力,使得GPU越来越易于满足通用计算的需求。
GPGPU是一种利用处理图形任务的GPU来完成原本由CPU处理(与图形处理无关的)的通用计算任务。由于现代GPU强大的并行处理能力和可编程流水线,令其可以处理非图形数据。特别在面对单指令流多数据流(SIMD),且数据处理的运算量远大于数据调度和传输的需要时,GPGPU在性能上大大超越了传统的CPU应用程序。
NVIDIA和AMD持续改进GPU的编程能力,尤其是CUDA和OpenCL推出后,基于CPU+GPU的异构并行计算越来越得到大家的重视。
GPU是为了渲染大量像素而设计的,并不关心某个像素的处理时间,而关注单位时间内能够处理的像素数量,因此带宽比延迟更重要。考虑到渲染的大量像素之间通常并不相关,因此GPU将大量的晶体管用于并行计算,故在同样数目的晶体管上,具有比CPU更高的计算能力。
CPU和GPU的硬件架构设计思路有很多不同,因此其编程方法很不相同,很多使用CUDA的开发人员有机会重新回顾学习汇编语言的痛苦经历。GPU的编程能力还不够强,因此必须要对GPU特点有详细了解,知道哪些能做,哪些不能做,才不会出现在项目开发中发觉有一个功能无法实现或实现后性能很差,从而导致项目中止的情况。
由于GPU将更大比例的晶体管用于计算,相对来说用于缓存的比例就比CPU小,因此通常局部性满足CPU要求而不满足GPU要求的应用不适合GPU。由于GPU通过大量线程的并行来隐藏访存延迟,一些数据局部性非常差的应用反而能够在GPU上获得很好的收益。另外一些计算访存比低的应用在GPU上很难获得非常高的性能收益,但是这并不意味着在GPU实现会比在CPU上实现差。CPU+GPU异构计算需要在GPU和CPU之间传输数据,而这个带宽比内存的访问带宽还要小,因此那种需要在GPU和CPU之间进行大量、频繁数据交互的解决方案可能不适合在GPU上实现。
4.移动设备
目前高端的智能手机、平板使用多个ARM核心和多个GPU核心,运行在移动设备上的应用对计算性能需求越来越大,而由于电池容量和功耗的原因,移动端不可能使用桌面或服务器高性能处理器,因此其对性能优化具有很高需求。
现在移动设备主要使用基于ARM的处理器,目前市场上的高性能ARM处理器主要是32位的A7/A9/A15。ARM A15 MP是一个多核向量处理器,它具有4个核心,每个核心具有64KB一级缓存,4个核心最大可共享2MB的二级缓存。ARM支持的向量指令集称为NEON。NEON具有16个长度为128位的向量寄存器(这些寄存器以q开头,也可表示为32个64位寄存器,以d开头),可同时操作向量寄存器的16个字节,因此使用向量指令可获得更高的性能和带宽。
现在移动设备上的GPU主要是高通的Adreno系列和Imagination的PowerVR系列,这两家厂商的大多数移动GPU已支持使用OpenCL进行并行计算,笔者也使用OpenCL在两家的移动GPU上编写过一些并行代码。