多核普及的致命伤 服务器软件的并行化革命

    从Power、UltraSPARC T1、安腾到双核Opteron、Xeon,各个领域都显示出,多核处理器计算平台势必成为服务器的主流或者说是强势计算平台,但上游硬件厂商的乐观预期似乎还有些过早??并不是所有的操作系统和应用软件都做好了迎接多核平台的准备,尤其是在数十年来均为单一线程开发应用的x86服务器领域。
  
    微软软件架构师Herb Sutter曾指出:软件开发者对多核处理器时代的来临准备不足。他说,软件开发社区认识到处理器厂商被迫采用多核设计以应对处理器速度提升带来的发热问题,但却没有清楚地了解这样的设计为软件开发带来多少额外的工作。
  
    在过去一段长时间里,x86系统上软件的性能随着来自Intel和AMD处理器速度越来越快而不断提高,开发者只需对现有软件程序作轻微改动就能坐观其性能在随着硬件性能的上升而不断提升。不过,多核设计概念的出现迫使软件世界不得不直面并行性(将单个任务拆分成多个小块以便分别处理之后再重新组合的能力)问题。当然,为服务器设计软件的开发者已经解决了一些此类难题,因为多核处理器和多路系统在服务器市场已经存在多年(在传统的Unix领域),一些运行在RISC架构多核多路系统上的应用程序已经被设计成多线程以利用系统的并行处理能力。但是,在x86领域,应用程序开发者多年来一直停留在单线程世界,生产所谓的“顺序软件”。
  
    现在的情况是软件开发者必须找出新的开发软件的方法,面向对象编程的兴起增加了汇编语言的复杂性,并行编程也需要新的抽象层次。
  
    另一方面,处理器设计厂商在设计产品时也应该将软件开发者考虑在内,“处理器的首要着眼点应该是可编程性,而不是速度。”Sutter说。
  
    多核处理器要想发挥出威力,关键在于并行化软件支持,多核设计带动并行化计算的推进,而给软件带来的影响更是革命性的。
  
  Intel很早就通过超线程技术实现了逻辑上的双处理器系统,可以并行计算,但这不过是对处理器闲置资源的一种充分利用而已,并且这种充分利用只有在特定的条件下,尤其是针对流水线比较长且两种运算并不相互交叉的时候,才会有较高的效率,如编码解码、长期重复某种矩阵运算以及一些没有经过仔细编写的软件等。
  
    即使IBM的Power5架构,也需要跟最新的操作系统进行融合,加上运行在其上的软件,才有可能利用并发多线程。
  
    虚拟化技术在一定程度上能够处理一些因为多核带来的问题,可以让应用软件和操作系统在透明的环境下对处理器资源进行分配和管理。
  
    目前在对称多处理器方面,操作系统对资源的分配和管理并没有本质的改变,多以对称的方式进行平均分配。也就是说,在操作系统层面,当一个任务到来时,剥离成为两个并行的线程,因为线程之间需要交流以及操作系统监管,它导致的效率损失要比硬件层面大得多。并且,多数软件并没有充分考虑到双核乃至多核的运行情况,导致线程的平均分配时间以及线程之间的沟通时间都会大大增加,尤其是当线程需要反复访问内存的时候。目前,多数操作系统还没有完全实现自由的资源分配,如IBM是通过AIX 5.3L来支持Power5上的虚拟化功能,才实现了资源的动态调配和划分的。
  
    从长远来看,需要使用虚拟化技术才可能实现操作系统对任务的具体划分,这很可能改变一些通用的编程模式。
  
    面对多核系统,需要有并行编程的思想才有可能充分利用资源,而人类的思维模型习惯于线性思维,对“面”或者更为复杂的立体编程模式,效率会下降很多。
  
    软件在并行化方面的滞后给多核处理器技术的发展蒙上了一些阴影。尽管用户在充满希望地期待着,但思维的改变不是一朝一夕的事情。
  
    服务器端的并行化应用设计似乎已经解决了一些,但仍需要努力提高其扩展能力。
  
    并行化对于客户端的应用是一项巨大的挑战,而对于许多基于服务器的程序来说,却是一个“已经圆满解决的问题”。我们在服务器上使用的并行化架构一直在很好地工作,但要想确保这类架构的顺利编程和扩展能力,人们仍然需要付出巨大的努力。这些应用通常都具有很强的并行特性,它们可以同时处理许多独立的请求流。例如,一台Web服务器或一个Web站点可以独立执行同一代码在大部分不重叠数据上的数千个版本。此外,这些执行都是相互独立的,通过一个抽象数据存储来共享状态,例如通过支持高度并行化访问结构化数据库。今天,表达并行的方式有很多种,每种方式只适用于一个特定的程序子集。这些并行编程模型在两方面的区别比较大:并行运行的粒度和任务间相互配合的程度。
  
    并行执行的操作可能是单个指令,如加法或乘法,也可能是需要用几天时间来运行的复杂程序。很明显,对于小型操作来说,并行基础设施的开销成本是非常巨大的,总的来说,任务分割得越小,将其生成为单个任务和提供通信及同步时所需要的成本就越高。
  
    另外一个方面就是操作间通信和同步配合的程度。最理想的程序是没有任何的配合,操作完全是独立运行的,而且产生的结果也是完全不同的。在这种情况下,操作可以按任何顺序运行,也不会产生任何同步或通信成本,在编程时无须考虑数据竞争的问题,编程过程自然也就会非常容易。然而,这种状态是非常罕见的,多数并行程序都要在操作之间共享数据。当操作变得越来越多样化时,确保正确和有效的操作其复杂性也会随之提高。最简单的案例是为每个操作执行相同的代码,这种共享类型是一种无规律的并行方式。更具有挑战性的是,无规划的并行方式,在这种方式中,操作是完全不同的,而且共享方式也更难以理解。
  
    独立并行 在这种方式下,一种或多种操作是针对数据集中的每个项目独立实施执行的。精细的数据并行依靠的是操作的并行执行,它们不应当共享输入的数据或结果,并应当在无协调的情况下完全可以执行。
  
    在实际应用中,搜索引擎等应用只共享一个大型的只读数据库,因此并发的处理检索不需要任何协调,同样,大型模拟通常需要在运行时分析大量的输入参数,也是一种独立并行模型。
  
    有规律的并行 比独立并行更复杂一些的是当运算之间存在相互依存关系时,将同一种运算应用到一个数据集上。如果两个运算之间存在通信或同步,则在数据上某一部分执行的运算会与另外一个运算存在依存关系。有规律的并行程序要想取得正确的结果,需要同步或认真协调执行策略,与一般并行不同的是,我们可以对这类操作背后的代码进行分析,确定如何以并发的方式对其加以执行,以及确定它们共享哪些数据。
  
    无结构的并行 无结构的并行对数据的访问是不可预见的,而且需要通过明确的同步方式对其加以协调。在使用线程和明确的同步方式写成的程序中,这种并行形式最为普遍。在这类程序中每一个线程都有自己特定的职责,我们很难讨论这类并行形式中任何具体的内容,但有一点:两个线程在访问数据时如果发生冲突,就必须使用明确的同步方式来解决,否则,程序将进入无法确定的状态。
  
    实现软件并行化,需要更好的编程语言,需要更高层的语言抽象,使现有的应用能够以一种渐进的方式转变成并行化的应用。
  
    在编程语言中我们需要什么?需要更高层的语言抽象,包括现有命令式语言的发展性扩充,使现有的应用能够以一种渐进的方式转变成并行化应用。这种编程模型必须将并行化转变成一种易于理解和分析的形式,而且不仅是在最初的开发阶段,在维护阶段更是如此。
  
    明示、暗示和自动并行 明示的编程模型可以提供抽象的方法,并要求编程人员能够明确地说明并行在哪里发生。明显表达并行的主要优势在于,它允许编程人员充分利用应用领域的知识,并充分表达应用中潜在的并行性。
  
    然而,它要求新的、更高一层的编程抽象,并且在处理共享数据时需要更高层次的编程能力。
  
    暗示的编程模型将并行性隐藏在库中或API(应用程序接口)后面,因此调用方看到的仍然是顺序的形式,而由库去执行并行操作。它的主要缺点是,这种方法不可能实现某些与并行有关的性能提高。此外,这种方式也很难设计出一种在任何情况下都不显示并行性的界面。
  
    人们还在广泛研究另外一种方法,这就是自动并行处理。在这种方法中,编译器将负责查找并行,通常是在那些以Fortran等传统语言写成的程序中。这种方法从表面上来看非常具有吸引力,但在实践中并不能很好地工作。要想理解程序的潜在行为,精确的程序分析是必不可少的。即使是对于Fortran这种简单的语言来说,这种分析也是非常具有挑战性的。此外,顺序程序通常都使用顺序算法,其中包含的并发特性极少。
  
    命令式和功能式语言 常用的商业编程语言(如Pascal、C、C++、Java、C#)都属于命令式的语言,即由编程人员规定变量和数据结构中的变化。函数(如循环)、低级数据操作和共享式的可变对象实例都会使这些语言编写而成的程序很难分析和自动并行执行。一般人都相信,功能语言,如Scheme、ML或Haskell可以消除这种困难,因为它们天生就适合并行运行,用这些语言编写的程序可以操纵不可变的对象实例,而在实践中,功能语言并不一定能够给并行执行带来益处。功能程序中的并行通常是在过程调用层面上的,而且为了适应传统的多处理器,这些过程被分割成了粒度非常精细的程序,几乎达到了不切实际的地步。功能语言对并行处理的真正贡献在于,这些语言通常使用的都是更高层次的编程风格,而在这种风格中,Map和Map-reduce等操作会将计算应用于集合数据结构的所有组件,这些较高层次的运算都是并行执行的资源。例如,Google的高级工程师就曾经描述过Google是如何使用Map-Reduce来进行大规模分布式计算的。命令式语言可以将功能扩展添加进来,这一点非常重要,为了保留目前的各类软件中的巨大投资,用户自然希望以渐进的方式逐步添加对并行处理的支持。
  
    更好的抽象 今天的多数语言提供的是线程和锁定层面上的明示编程方式,这些抽象都属于低层次的。较高层次的抽象允许编程人员表达那些具备固有并行特性的任务,而运行时系统就可以对其进行组合和调度,使其适合实际机器上的硬件,这样就可以使应用能够在比较新的硬件上发挥更好的性能。
  
    高层次抽象的另外一个例子就是活动对象。活动对象在概念上运行在自己的线程上,因此创建1000个此类对象就相当于从概念上创造1000个潜在的执行线程。活动对象的行为方式与监视器非常像,但它不需要传统的锁定。相反,活动对象以外的方法调用都是异步信息,由对象对其进行汇集、排队和传送。
  
    开发人员已经设计并实现了一些有趣的编程模型,有助于开发并行应用程序,最流行的就是用于共享内存编程的OpenMP和用于分布式内存编程的MPI。 
  
  


  
    OpenMP是一种工业标准的API设计规范,是由Sun、HP、IBM和Intel等多家顶级计算机厂商和软件开发商联手推出的,其目的在于为软件开发人员提供一种通用的规范,使其可以很方便地设计新并行应用程序或修改及并行化现有串行应用程序,从而利用配置了多处理器计算系统的共享内存。可移植性也是OpenMP的主要目标之一,使用OpenMP开发的并行应用程序源代码可由支持OpenMP的任何编译器编译,且编译好的二进制代码可在目标硬件平台上运行,以获得出色的并行性能。
  
    最流行的本地编程语言Fortran和C/C++都支持OpenMP。左图给出了分别以C/C++和Fortran编写的简单OpenMP程序示例。在本例中,将y数组加到x数组这一循环迭代操作,是以并行方式执行的。源代码中的编译指示、指令和编程API调用表示了OpenMP的结构。OpenMP 的结构允许程序员指定并行区域、同步和数据作用域属性,它还支持用于指定运行时配置的环境变量,例如,环境变量OMP_NUM_THREADS指定了运行时所使用的工作线程的数量。
  
    由于OpenMP编程模型专用于单一进程,因此其可伸缩性最终要受到一台计算机中处理器(线程)数量的限制。可伸缩性还会受编程逻辑复杂程度和编程风格的限制。当前的OpenMP语义尚不够灵活,无法处理某些应用程序。例如,当前的OpenMP规范仅允许在并行区域内创建有限的动态线程。
  
    MPI(Message Passing Interface,消息传递接口)是一种工业标准的API规范,专为在多处理器计算机和计算机集群上获得高性能计算而设计,该标准是由大量计算机供应商和软件开发商共同设计的。有多种来自不同研究机构和厂商的MPI实现,其中最流行的一种就是MPICH,MPICH常用作为特定平台或互连而优化的MPI实现的编码基础。MPI实现依然在不断发展。MPI-1支持一些关键特性,如点到点及与通信设备的集群消息通信。一条消息中可包含基本数据类型或派生(用户定义的)数据类型的MPI数据,它还支持互连拓扑。MPI-2则提供许多高级通信特性,如远程内存访问和单端通信等,还支持动态进程创建、管理和并行I/O。
  
    总体而言,MPI为计算机集群上的并行应用程序提供了一个出色的解决方案,但对于许多开发人员来说,MPI也是一种困难的编程模型,因为MPI的通信延迟时间较长,所以必须合理分割程序的核心逻辑,以使分布成本更为合理。分析及划分应用程序问题,并将问题映射到分布式进程集合中,绝对不是一项可靠直觉完成的任务,因为许多MPI进程之间的交互作用都相当复杂,因此即便选用了恰当的工具,调试并调整运行在大量节点上的MPI应用程序也极具挑战性。MPI的性能取决于底层硬件平台和互连,在某些极端的情况下,MPI应用程序的性能可能会受到繁重的互连流量的影响。另外一个严重的问题就是大规模MPI应用程序的可靠性。对于许多MPI实现而言,只要单一计算节点发生故障,无法正确响应,MPI程序就会停止工作。(本文内容由Sun软件架构师 Liang T?Chen和微软软件架构师Herb Sutter提供)
  
  编看编想
  
      大规模的并行化来了
  
  姜波
  
    在服务器端,RISC架构的服务器进入多核技术领域要早一些,所以一些专有的、或者说是基于Unix操作系统的行业化特征非常显著的应用已经是并行化编写的。而当x86领域迎来64位计算和多核技术的时候,绝大多数的基于Windows的应用都是传统的按照单一线程开发的。
  
    现在,标准化、开放、TCO等浪潮席卷整个计算领域,x86服务器市场在飞速发展,逐步挤压原来的、非常强势的RISC架构服务器占有的份额,跟随服务器硬件技术的发展脚步,基于x86服务器的操作系统以及上层应用,都需要考虑处理器级别的多核、并行计算设计带来的性能提升,考虑软件如何能够适应并且更充分发挥硬件架构的优势。应该说,这是软件层进入了大规模的并行化设计阶段,毕竟,在销售量方面,x86市场是绝对领先的。
  
    记者之前接触过一些将应用改为并行运行的案例,是采用集群系统后,将原来的应用进行一些修改以便能够充分利用集群系统并行处理的优势,比如石油勘探行业的用户,他们在计算和分析地震勘探资料应用中,采用大规模并行计算系统来实现叠前偏移和精确地震成像处理,提高勘探开发效益。不过,这些应用都还处在一个刚刚开始的并行化应用阶段,采用的是集群系统。多核技术在x86服务器中成熟后,如何能够充分利用到多核并行处理的优势,才是软件层面真正的挑战,就是所谓的“线程级并行”。
  
    处理器在并行计算方面所做出的技术革新贡献,仅仅是整个产业链上的一个小环节,与用户应用紧密相连的是软件层,正如文章中引述的微软软件架构师Sutter所说的:处理器设计首要的着眼点应该是可编程性,而不是速度。
  
相关链接
  
    真实案例研究 并行搜索
  
    要在大量符合某条件的问题中查找最优解决方案,或所有可枚举出的解决方案,最流行的一种方法就是树搜索。许多科学问题或日常生活中遇到的问题都可以转换并表达为一个并行树搜索问题。可能的应用程序范围从微不足道的解谜一直到复杂的战略分析。一种流行的方式就是建造一个状态树,每个节点表示一种特定的应用程序状态,然后对状态树进行搜索,找到具有最优状态的目标节点,或经过最少的步骤抵达目标节点。
  
    为这样的并行搜索应用程序开发软件共有两个步骤。第一步是创建一个良好的抽象模型和转换问题,并以状态树的形式将其表现出来。抽象模型的质量将影响树的形状和大小,也会影响搜索的难易程度。第二步是使用并行计算在整个状态树内搜索。在大多数情况下,树都是在程序运行时动态建造的。由于树易于以一种不均衡的方式成长,因此随机地将一个子分支指派给工作线程或进程,可能会失去负载均衡。
  
    总体而言,概括并行搜索编程非常困难,从模型抽象开始,我们就要面对挑战,随后是树建造,最后是并行搜索算法。一个出色的树模型需要开发人员具备指定行业的丰富经验,将问题完备地表达出来,并用树节点的形式表现问题。建造状态树后,通常还需要对树模式进行试验分析,以设计出一种有效的并行搜索算法。算法通常需要对树节点进行分割和分组,以发挥并行性能。