免费午餐的终结:软件迈向并发时代

来源:互联网 发布:javascript输出乘法表 编辑:程序博客网 时间:2024/04/28 15:12

免费午餐的终结:软件迈向并发时代

Herb Sutter

       OO革命以来软件开发领域最重大的变革已经迫在眉睫了,它就是并发

      

       很快你就将不能享受免费的午餐了。对于这点,你能做些什么呢?你又将做些什么呢?

       在用传统的手段提升CPU性能这方面,主要的处理器生产厂商(IntelAMD)和主要的处理器架构(SparcPowerPC) 已经江郎才尽,很难有什么大的作为了。于是他们一致决定采用超线程技术或多内核()架构而不是继续提高时钟速度和指令流量(straight-line instruction throughput, 指顺序执行的指令的流量)的方法来提高性能。超线程技术和多内核架构已经在当前的芯片中得到了应用。特别是多内核架构,已经被当前的PowerPCSparc IV处理器所采用。到2005年,IntelAMD也将采用这一技术。实际上,2004In-Stat/MDR秋季处理器论坛(In-Stat/MDR Fall Processor Forum)最核心的议题即是多内核设备,相当多的公司展示了他们新设计的和改进的多内核处理器。现在看来,称2004年为多内核年也不为过。

       然而这也把软件开发推到了一个转折点上,至少在接下来的几年里,面向桌面通用计算机和低端服务器的应用(主要是这些应用构成了当今极其庞大的软件市场)将会受到其影响。在这篇文章里,我将描述硬件结构的改头换面,这些改变将如何影响软件,特别是会如何影响你以及你将来编写软件的方式。

       事实上,所谓的免费午餐在一两年之前就已经没有了,只是我们没有注意到罢了。

       性能上的免费午餐

       计算机界有一个相当有趣的现象叫作:“Andy giveth, and Bill taketh away.” 这句话可以理解为Intel给多少,微软就会吃掉多少(AndyIntel的前总裁格罗夫,而Bill当然就是指微软的那个让人又恨又爱的比尔了)。也就是说,无论处理器的速度多块,软件总是有办法消耗掉这部分多余的性能。CPU的速度提高了10倍,而软件需要做的工作量也增大了10(甚至于在某些情况下会以某种更为低效的方式完成这10倍的工作量)。大部分的应用程序已经享受了几十年的免费的、有规律的性能提升,而且通常它们什么也不用做。不需要发行新版本的程序,也不需要做什么特别的工作,因为硬件厂商就可以帮它们实现这一目标。CPU厂商(居功至伟)、内存和磁盘厂商(贡献居于第二位)稳步的使更新更快的系统成为应用的主流系统。时钟速度并不是衡量性能的唯一指标,但却是一个非常好的、必要的、很有指导价值的一个指标。我们已经习惯了CPU时钟速度的大幅度跃升,从500MHz1GHz再到2GHz,依此类推。到今天,拥有3GHz等级CPU的计算机已经成为了主流产品。

    关键的问题是:这一切何时是个头?毕竟,摩尔定律所预言的CPU的性能提高是随着时间而成指数性增长的。然而指数性的增长肯定不可能是永久性的,有一天我们必将达到物理器件的性能极限。即使我们采用光技术来设计CPU,也总有一天到达物理器件的极限而不能进一步提高性能。CPU性能的增长肯定会慢下来并最终停止增长。(当初摩尔所预言的指数增长主要是指芯片上的晶体管密度,但是CPU的时钟速度的增长也遵守了这一规律。这一定律还可以用于其它方面,比如存储器容量的增长,甚至比CPU性能的增长速度更快。当然,关于这一重要趋势的描述应该属于另一篇不同的文章了。)

    如果你是一个软件开发者,你可能已经或正在享受CPU性能快速提升这一免费大餐。你的应用程序的性能仅仅取决于少数的本地操作(local operations),对吗?也许你会说:“别担心,好日子终将继续,明天的处理器会更强大,具有更大的吞吐量;当前的应用程序的性能更多的是受到CPU速度和内存速度以外的其它因素的影响(比如IO的速度、网络的速度和数据库的速度)”。这么认为正确吗?

    这么认为当然是正确的,但是这只是指过去。而在可预见的将来,这种看法有可能是完全错误的。

    这里有一个好消息和一个坏消息。好消息是处理器的性能会一如既往的变得更加强大;坏消息则是至少在近期一段时间内,处理器性能的快速增长并不能使应用程序的性能得到同等程度的显著提高。

    在过去的30年间,CPU的设计者们主要通过三方面的措施来保证CPU的性能提升,它们分别是:

Ø         时钟速度;

Ø         执行优化;

Ø         高速缓存。

这其中前两方面的措施主要是为了使直线型的指令流的执行更加顺畅。增加时钟速度使得有更多的时钟周期可用,使CPU运行的更快或多或少的意味着在相同的时间内完成更多的工作。优化指令流的执行意味着在每个时钟周期内可以做更多的工作。今天的CPU支持一些非常强大的指令,它们执行一系列的从最呆板到最新奇的优化操作,包括流水线技术、分支预测、多发射(一个时钟周期内执行多条指令)执行,甚至包括指令的乱序执行。这些技术的目标主要是使指令流变得更好、执行更顺畅、快速;通过减小时钟延迟和指令的多发执行来达到在相同的时间内执行更多的指令。

CPU的设计者们面临的要他们提供威力更强大的处理器的压力如此之大,以至于他们会冒着改变你的程序含义甚至颠覆程序本意的风险来采用各种技术提高CPU的执行速度。

简短的浏览一下这些优化技术,包括指令重排序和内存模型重整,请注意这里面的一些技术甚至不能称为优化技术,因为他们会改变程序的语义,有可能使你不能得到预期的程序执行结果。这是显而易见的,因为CPU设计师一般来说都很善良,具有良好的自我调节的能力,他们不会去故意破坏你的代码---当然,只是从一般意义上来说。但是近些年来,这些老兄却采取了一些侵略性的十分霸道的优化技术,即使他们完全知道这些优化技术会改变你的程序的语义也在所不惜。难道过去的Hyde先生(Hyde可能指海德·爱德华,1609-1674英国政治家,英国内战期间任查尔斯一世的首席参政,1660-1667年任大法官,我怀疑他可能是一个喜欢越俎代庖或喜欢胡乱做事的人)又重现人间了?当然不是,这种自发的热情完全是因为芯片设计师们在发布更快速的CPU方面面临着极其巨大的压力。在这种巨大的压力下他们甚至与甘冒改变程序的语义甚至完全颠倒程序含义的风险来采用各种优化技术,仅仅是想使CPU的执行能更快一点。这方面两个典型的例子就是写重整(write reordering)和读重整(read reordering)技术:允许进程对本来有顺序关系的写操作重新排序执行。这种技术是如此的奇特以至于经常让大部分程序员大吃一惊,不知所措。在通常的情况下人们不得不关闭此项特性,因为它使得程序员正确理解自己程序的语义变得十分困难。读重整操作也经常会带来令人惊讶的影响,但是一般说来人们还是愿意保留这项操作,因为它对程序员来说并不是特别的难以掌握。这也使得操作系统和操作环境的设计者们采用这种给程序员带来很大负担的技术,因为他们觉得以合理的方式提升性能更重要,只能“两害相权取其轻”了。

最后,增加片上缓存意味着使CPU远离RAM。现在主存的速度越来越不能和CPU的速度相配,为了提高CPU性能必须使数据和处理器相距更“近”---还有什么比把数据直接放在芯片上更好的办法呢?!片上缓存的容量正在飞速增加,当今的主要CPU生厂商都提供具有2M或更多片上二级缓存的CPU(在前面的三种提升CPU性能的方法中,只有提升缓存数量这一手段在近期内有望继续得到发展。我将在后面花一点功夫介绍一下缓存的重要性。)

那么,所有的这一切又意味着什么呢?

你需要意识到的一件非常重要的事情是所有以上这些措施都没考虑兵法的情况。在以上任何一方面的进步都会直接加速顺序执行程序(非并发的、单线程或者单进程的程序)的执行,对确实利用了并发性的应用程序也一样有效果。这是非常重要的,因为现在几乎所有的程序都是单线程的,我将对这点在后面做进一步探讨。

当然,编译器也需要进行改进以追上技术变革。也许你需要重新编译你的应用程序,指定最低版本的CPU,以便从某些新的指令集(例如MMXSSE)CPU的一些新特性中获益。但是从总体上来说,即使那些老版本的程序也比过去运行的更快---甚至都不用利用最新的CPU所提供的那些指令集和新特性。这世界是如此的美好!但是非常不幸,这样的世界已经离我们远去了!

面临的障碍以及你为什么不能在现在就拥有10GHzCPU

CPU的性能提高大约在两年以前就已经徘徊不前了。大多数人是一直到最近才意识到这个问题。

你可以从其它芯片制造者那里得到类似的数据,在此我引用Intel提供的数据。图1显示了芯片的晶体管密度和时钟频率的历史数据的变化趋势。到现在为止,晶体管密度仍然在节节爬升,但是时钟频率的变化确实另一番光景。

在图1中,你会发现在2003年开始的地方时钟频率的变化有一个明显的拐点。我已经在图中根据数据的变化趋势拟合了一条曲线来描述最大时钟频率的变化趋势,与细点划线(那是曲线理论上应该遵循的轨迹)相比,实际的曲线呈扁平状,表示增长速度明显趋缓。这说明,由于在物理方面的几种原因,比如发热量过大以至于热量无法耗散、功率消耗过大、还有漏电流问题,导致越来越难以得到更高时钟频率的芯片。

小插曲:你现在的工作站上的CPU时钟频率有多高?有10GHz吗?以Intel的芯片为例,我们达到2GHz的主频已经是很久以前的事了(20018月份),以2003年以前CPU时钟频率的变化趋势,现在(2005年早期)我们应该已经用上了10GHzIntel家族的芯片。简单的回顾一下我们会发现,事实完全不是这样,10GHzCPU甚至两个影子都没有,我们也不知道何时能造出这样的芯片来。

Figure 1: Intel CPU Introductions (sources: Intel, Wikipedia)

那么,4GHz怎么样?我们已经有时钟频率3.4GHz的芯片了,4GHz应该不会太遥远了吧?哎,即使是4GHz的时钟频率,现在看来也是可望而不可及。也许你还记得,在2004年年中的时候,Intel曾经将4GHzCPU的发布推迟到了2005年,可是到了2004年秋天,Intel通过官方途径宣布完全放弃4GHzCPU的研制计划。在写作本文的时候,Intel决定再稍稍向前迈一小步,到2005年将时钟频率提高到3.73GHz(1曲线中向右上转折的部分)。在时钟频率上一争短长至少现在看来已经告一段落了。Intel和大多数处理器制造商都不约而同的将他们的未来压在了多内核这一方向上。

可能有一天4GHzCPU会出现在主流左面计算机中,但不会是2005年了。诚然,Intel已经在实验室中制造出了更高频率的CPU,但是这些CPU的正常运行需要极其苛刻的条件,比如不切实际的致冷设备。你不可能在你的办公室里拥有那种水平的致冷设备,就更别提移动计算了。

世间没有免费的午餐:摩尔定律以及下一代CPU

(TANSTAAFL: Moore’s Law and the Next Generation(s))

世间并没有免费的午餐---R. A. Heinlein, The Moon Is a Harsh Mistress

难道这一切意味着摩尔定律的终结吗?有意思的是,答案是“否”。当然,像其它那些以指数增长的定律一样,总有一天,摩尔定律要终结,但现在看来这种终结还不会很快出现。即使芯片工程师已经榨干了时钟频率提高方面的每一分潜力,但是片上的晶体管数量看来仍然会以指数增长,并且CPU的能力(throughput)在未来的一段时间内仍然会以某种“类摩尔定律”的方式继续增长。

关键的差别,也是本文的核心内容,是下两代的CPU性能的提高会通过数种完全不同于前述方式的道路来进行。也就是说,如果不进行重新设计,现有的应用程序不会再享受免费的午餐(指不用做什么,只要使用更先进的CPU即可大幅度提高程序性能)

从近期来看,芯片性能的提升主要会通过以下三种途径,这三种途径里面只有一种过去曾经使用过。下面是今后提升芯片性能的三种方式:

Ø         超线程;

Ø         多内核;

Ø         缓存。

超线程是指在单一的CPU中同时并行运行两个或更多个线程。现在市面上已经有超线程CPU了,它们也确实会并行执行某些指令。但是一个限制性的因素是现在的超线程CPU虽然拥有某些而外的硬件结构,比如额外的寄存器组,但是它们仍然只有一套缓存、一个整数数学单元、一个浮点协处理器(FPU),也就是说,那些CPU的基础的关键的组件(模块)它们都只有一套。对于合理书写的多线程程序,超线程技术能带来5%-15%的性能提升;对于经过仔细设计的多线程程序,在理想的状态下最多可以带来40%的性能提升。这听起来确实不错,但是它根本没达到性能翻倍的目标,对传统的单线城应用程序也没有任何帮助。

多内核处理器是在单一芯片上集成两个或更多的CPU。一些芯片,像SparcPowerPC,已经产出了多内核版本的处理器。IntelAMD的初始版本的多内核处理器预计会在2005年出现,估计它们只会在集成水平上有所差异,而在功能上则大同小异。AMD估计在性能设计上会有一些优势,比如片上集成技术,而Intel的初始版本只是把两个至强CPU捆到了一片单一的芯片上。双核设计性能的提升大致相当于一个双CPU系统,但是双核设计会比较便宜,因为它与双CPU系统相比,主板上只需要较少的CPU插槽和相关的芯片组。同时,这也意味着即使在理想的状态下它也不能达到完全的两倍性能提升,并且,它也只对经过细心设计的多线程程序才有效,对单线程程序则没什么效果。

最后,片上缓存的容量在近期会继续增长。在以上三种方式中,只有增大片上缓存这种方式会对大部分应用程序有益。不断增大的片上缓存的容量具有不可置信的重要性,它会加速大多数应用程序的执行,简而言之,就是以空间换时间的战略。主存的访问非常费时(当然是与访问缓存相比),除非有绝对必要,否则你不会愿意去碰RAM的。在当今的系统中,如果发生了缓存未命中的情况,则需要到主存中去读数据,这通常需要多花费1050倍的时间。这也使许多人感觉有点不可思议,因为我们一般认为主存是足够快的。主存当然很快,但分跟谁比,如果跟磁盘、网络比起来,主存速度当然要高的多,但是再高又怎能和在片上全速运行的缓存相比呢。如果一个应用程序的工作窗的大小匹配于缓存大小,我们将会得到最高的运行性能,反之亦然。这也就是为什么增大缓存的容量,即使我们不对原始代码做任何修改,那些运行多年的程序照样能焕发青春的原因。因为现存的程序需要操作越来越多的数据,它们被持续的更新以使之具有更多的特性,这些性能敏感的操作需要放进缓存中才能得到高性能。那些令人沮丧的就时代肯定能提醒你:缓存就是一切。

(另外在这里多说一句,最近又一件很有意思的关于以空间换时间的事情深深的震动了我们的编译器团队。32位和64位的编译器使用相同的源代码,只是分别被编译成32位和64位的程序而已。64位的编译器得到的程序在64CPU上运行具有相对来说比较高的性能基准(baseline,也就是说程序运行的平均性能较高),这主要是因为64CPU里面有丰富的寄存器资源和更多其它有利于提高性能的特性。这一切都不错,但是数据怎么样呢?移植到64位平台,大部分内存中的数据还是保持原样,没什么变化,但是指针的尺寸恰好是32位的时候的两倍。而恰好,我们的编译器又跟其它的应用程序有点不太一样,它大量的将指针特性用于其内部数据结构。由于指针的尺寸从4比特长到了8比特,所以我们发现我们的编译器的工作窗口明显加大。这更大的工作窗口导致抵消了本来从更好的处理器和更多的寄存器得来的性能上的提升。在写作本文的时候,我们的64位编译器和32位编译器运行速度基本相同,虽然64位处理器本来具有更好的原生的处理能力和处理流量。也就是说,以空间换时间。)

但也只有缓存这样,超线程技术和多内核技术并不影响现有的大多数程序的性能。

但是这种硬件上的变化如何影响软件的编写呢?也许你已经知道大概的答案了,下面我们就将详细讨论这一问题及其后果。

小插曲:神话和现实2 x 3GHz < 6 GHz

一个由两个3GHz的内核组成的双核CPU可以提供6GHzCPU所提供的处理能力,对吗?

错了!即使两个单独的线程运行在两个单独的处理器上也不意味着可以提供2倍的处理能力。同样可知,大多数的多线程应用程序在一个双内核的系统上运行也不会得到两倍的运行性能。它的运行应该比在一个单核的CPU上快一点,但是这种性能的提升并不是线性的。

为什么会这样?首先,两个内核之间需要保持缓存的一致性(缓存的数据之间以及缓存和主存之间)和进行其它的握手(handshaking)活动。今天,一个具有两个或者四个CPU的计算机并不能提供两倍或者4倍于单CPU的计算机的性能,即使对多线程应用也是如此。这和在单一芯片上的多个CPU核的情况类似。

第二,除非此双核CPU上运行了两个单独的进程或者两个基于同一进程的独立线程,并且程序的代码经过仔细的调整,使这两个线程之间极少或根本不发生等待的情况,这是双核CPU的威力才能显现出来(如果不考虑这些,我要指出的是,当今很多单线程程序移植到双核系统之后行能有了一定程度的提升。这种提升不是因为多出来的这个核做了些什么有益的事,而是因为现在大多数系统上存在大量的广告软件和间谍软件,这严重影响了单核CPU的性能。关于加一个CPU来运行这些广告软件和间谍软件是否会提高系统性能,我留给你自己去做判断)

如果你运行的是一个单线程的应用程序,这次程序只能利用一个核。我们在这种情况下应该能发现程序性能有一定的提升,因为操作系统和应用程序可能分别运行在一个单一的核中,但是操作系统并不能总是最大化的利用一个核,而让另一个核保持空闲(Idle)状态(并且,间谍软件在大多数情况下一般和操作系统共享一个核)

所有这些对软件意味着什么:下一次革命

20世纪90年代,我们深入的学习了面向对象技术。主流的软件开发从结构化编程转到面向对象编程是软件开发界过去20年来发生的最大的变化,说是过去30年来最大的变化亦无不可。当然这些年来还发生了其它的很多变化,包括最近的web services(也十分的有趣),但是在我们的职业生涯中,没有任何一种变化对编写软件产生了如此根本性的、极其深远的影响,只有对象技术革命做到了这一点。

但是,从今天开始,性能的免费午餐我们已经不能继续享用了。当然,几乎每个人都能继续享有应用程序性能方面的提升,但是这种性能的提升全是赖缓存容量提高所赐。如果你想继续过去那种程序性能呈指数提升的美好时光的话,你必须十分仔细的编写你的应用程序,以并行的(通常为多线程)模式编写。这说起来比做起来要容易得多,因为没有什么问题是与生俱来具有良好的并行结构的,所以并行程序设计相对来说要困难得多。

我能听到那些嚎哭和抗议之声:“什么?并行?这一点都不新鲜。人们早就开始编写并行的应用程序了。”这当然很正确,但这只是指程序员里面的一小部分人。

要知道人们做面向对象编程是至少从Simula发明的20世纪60年代就开始了。但是在90年代以前,OO技术并没有发展成一场革命并统治主流软件开发领域。为什么OO革命发生在90年代呢?因为我们的工业是受需求驱动而发展的,而当时的需求就是我们需要越来越强大的CPU和存储设备,需要编写越来越大的系统来解决越来越复杂的问题。OOP在抽象和封装方面的强大能力使编写大规模的、经济的、稳定可靠的、可重现的软件系统成为可能。

我们编写软件的方式的下一波革命是并行性革命

类似的,我们的并行编程也是从那个黑暗时代开始的,包括协同程序、监视器(monitors)和其它一些类似的东西。在过去的十年或更长的时间里,我们已经见证了越来越多的程序员开始编写并行性的程序(多线程或者多进程系统),但是真正意义上的转向并行程序设计的革命并没有发生。今天的绝大多数应用程序依然是单线程的,对这一点,我将在下一节进行说明。

顺便说一句,作为大肆宣传的一个伎俩,人们总是给他们自己的技术冠以“下一波软件革命”的大招牌。不要相信这些伎俩。新技术确实很有趣并且在某些时候也能带来一些益处,但是那些使我们编写软件方式发生翻天覆地的革命性变化的技术往往是已经出现了很多年,经过了长时间循序渐进的增长之后才发生爆炸性增长的。其实这是很有必要的,软件开发革命必须以足够成熟的技术(有稳定的供应商和工具支持)为基础,并且应该经历大约七年左右的时间融入当时的新技术以使这些技术足够稳定、没有性能和其它方面的重大缺陷。因此,真正意义上的软件技术革命,比如OO,它们的相关技术都经过了数年甚至数十年的锤炼。甚至在好莱坞,那些真正的天才的“一夜成名”在其横空出世之前也都经过了多年的努力奋斗。

软件开发技术的下一波革命将是并行性革命。不同的专家对于并行性革命是否会超越OO革命这一点上依然有分歧,但是我们把这些争论留给那些无聊的学者吧(pundit指博学者或空谈者)。对于技术专家来说,令人感兴趣的是并行性革命无论在革命的规模(预期规模)还是革命的复杂性、学习新技术的曲线方面都与OO革命不相上下。

预期收益和并行性的代价

有两个主要的原因可以解释为什么并行性,尤其是多线程技术,已经在主流软件开发中得到应用。第一是在逻辑上将具有天然独立性的控制流分开,比如在一个数据库复制服务器中,很自然的我将每一个复制会话(session)放在一个独立的线程之中,因为每个会话对其它活动会话来说都是完全独立的(只要它们不操作数据库的同一行数据)。第二个不太显著的原因是基于性能的考虑,利用并发行的代码,我们可以充分利用多CPU(物理上的)的威力或者充分利用程序的等待时间(也可以理解为空闲时间,在这段时间之内某一部分程序暂时停止运行,不再占据CPU,这时我们可以利用CPU做一些其它的工作)。在我的数据库复制服务器中,对这一点就利用得很好,利用多线程技术可以充分利用多个CPU的能力,所以我们的服务器可以处理越来越多的与其它服务器之间的并发复制会话。

然而,并行性也是有代价的。一些很明显的代价相对来说并不那么重要。例如,锁(Lock)的使用代价就十分高,但是如果你明智的、正确的使用锁,找到一个合理的方法将操作并行化,减少或消除共享状态,那么你的所得收益还是会大于在同步方面所付出的代价。

并行性的第二位的代价也许是有一些应用并不适宜进行并行化,关于这一点我将在后面详细讨论。

并行性的最大的代价应该是并行化本身是十分困难的。编程模型,也就是程序员脑袋中那个推导和确认自己的程序的正确性的那么模型,其建立过程对于并行程序设计来说要比对于串行程序设计的顺序控制流来说要困难得多。

每一个学习并行程序设计的人都认为自己已经对其有了完全理解,但是一旦他们发现那些奇妙的古怪的他们本来认为完全不可能的竞争冒险的情况时,它们才能认识到,事情根本不是那回事,自己根本没有理解并行性。一旦开发者理解了并行性,并且发现这些竞争冒险可以被in-house 测试捕捉到的话,他们的知识水平就会达到一个新的高度并且会为此感到十分惬意。但是那些没有被in-house 测试捕捉到的漏洞(当然对于那些理解了为什么以及怎样进行压力测试的开发者除外)就会成为并行性漏洞(concurrency bugs)。这些漏洞只有在真正的多处理器系统上才会露面。在这些系统上,多个线程不是在一个单一的处理器上进行切换,而是真正的并发同时运行,这时那些没被发现的漏洞就会显现出来。下面的事实对那些觉得自己现在已经掌握了如何编写并发代码的人可能是一个打击:我已经遇到了很多这样的团队,他们的程序经过了非常严酷的压力测试,并且在许多客户那里都运行得十分正常。直到有一天,某个客户拥有了一台真正意义上的多处理器系统,这时那些奇怪的竞争冒险和运行错误就开始间歇性的蹦出来。以今天的CPU发展的光景来看,重新设计你的应用为多线程应用并且使之在多内核处理器上运行就跟直接跳进深水区去学游泳一样---直接到最严酷的环境,在一个真正的并行性系统上,程序所有的错误和缺陷都会显现出来。就算是你已经拥有了一个可以写出安全的并发代码的团队,仍然有许多陷阱需要注意。例如,你的并行性的程序运行起来相当的安全,但是却比运行在单处理器系统上快不了多少,这主要是因为线程之间的独立性不够,引用了某些共享的非独立的资源,而这会导致程序执行流程的变化,从而导致程序的效率不彰。这个问题十分的微妙。

今天的大多数程序员都没能深入的理解并行性,就像15年前的大多数程序员没有深入的掌握OO技术一样

就像一个结构化编程的程序员学习OO是一个很大的飞跃一样(这里的飞跃指很多观念都需要转变和重新学习,比如什么是对象?什么是虚函数?我应该如何来使用继承?在这些什么(whats)和怎样(hows)之上,为什么这些所谓正确的设计范式就是正确的?(why are the correct design practices actually correct?)),对于一个习惯了串行程序设计的程序员来说,学习并行程序设计同样会面临很大的困难(什么是竞争冒险?什么是死锁?它们是怎样出现的?怎样才能避免它们的出现?什么样的结构才可以serialize(连续化,序列化,应该是指调整程序的执行流程)那些我认为具有并行性的程序?怎样才能与消息队列成为朋友?在这些什么(whats)和怎样(hows)之上,为什么这些所谓正确的设计范式就是正确的?(why are the correct design practices actually correct?))

今天的大多数程序员都没能深入的理解并行性,就像15年前的大多数程序员没有深入的掌握OO技术一样。但是并行程序设计模型还是很好学的,特别是当我们坚持基于“消息”和“锁”机制进行程序设计时更是如此。一旦我们深入理解了并行程序设计模型,我们就会发现,与OO模型比起来,它也不是很难,并且并不是那么怪异,很自然。做好准备吧,投入一定量的时间,进行适当的训练,为了你,也为了你的团队。

(我故意的将上面这段限制在基于“消息”和“锁”机制的并行程序设计模型上。仍然有不基于“锁”机制(lock-free)的模型,在语言这个层次上,Java 5和流行的C++编译器都对其提供了直接的支持。但是现有的lock-free编程,与lock-based编程比较起来,对程序员来说仍然十分困难和难以理解、掌握、推导。在大多数的时候,掌握lock-free编程是那些系统和库的编写者的事,对于大多数程序员来说,直接用这些代码就可以了。其实坦率的讲,即使是lock-based编程,也是十分危险的(指编写的软件可能不具有预期的功能))

对我们来说有什么意义呢

好,现在开始谈谈对我们自身的意义。

1.              在上面我们主要讲到的是:高生产力(吞吐量)CPU现在市场上已经可以得到,并且在接下来的几年中,其计算能力还会继续稳步增长。如果我们的程序想要充分利用高性能CPU的优点,则必须对其进行并行化。比如,Intel已经在讨论100个核的CPU,一个单线程的应用到时将只能利用这样的CPU 1/100的计算能力。“噢,性能不是问题,计算机会变得越来越快”,这样的话语现在应该被视为幼稚并且值得怀疑的,在未来的几年中,这类话语就会被视为可笑的和错误的。

应用程序越来越需要并行化以充分利用呈指数增长的高性能CPU的计算能力,而且,性能和效率的优化会变得越来越重要而不是越来越不重要

现在,并不是所有的应用(严格的说法是应用中的重要的操作)都适合进行并行化。诚然,一些类型的问题,比如编译,非常适合进行并行化。但其它的则不是这样,这里有一个明显的反例可以说明这个问题:一个妇女需要9个月孕育一个婴儿,但是这并不是说有九个妇女的话就可以只用一个月的时间就把孩子生下来。你可能以前就碰到过这样的问题。但是你注意到了造成这种现状的原因吗?这里有一个狡辩性的(trick)问题你可以用来反问像上面那样问你的人:你就能得出结论说生小孩的问题(Human Baby Problem)是天生不具有并行性的问题吗?通常犯这类错误的人主要错在不该草率的得出此类问题为天生不可并行的结论,但是这类错误也通常无须纠正。如果说目标是只生一个小孩,则此问题是非并行性的。但如果说目标是生育多个小孩,则此问题则具有良好的并行性结构。知道了真实的目标就可以理解这中间的那些差别。当我们考虑是否和如何将我们的软件并行化的时候,谨记以目标为导向的基本原则是十分重要的。

2.              一个不太明显的影响可能就是:应用程序以后会或多或少的变成CPU相关的(CPU-bound),或者说其运行要受限于CPU的性能。当然,并不是应用中的每一个操作都会变成CPU相关的,那些本来没这种问题的应用也不会一夜之间全变成CPU相关的,但是看起来我们已经走到了如下这一趋势的尽头:应用程序的性能越来越变成I/O相关的、网络相关的或者数据库相关的(这里的相关依然沿用前述的概念)。因为这几方面的相关技术依然在飞速发展(考虑以下吉比特Wi-Fi,或其它的一些方面),而传统的增强CPU性能的技术已经走到了尽头。设想一下:我们在3GHz这一时钟频率范围内止步不前,单线程应用程序除了可以通过缓存的增大(这是最重大的好消息)得到适当的性能提升以外从现在开始,已经不能通过传统的手段使之变得更快。其它方面的性能可能仍然会有提升,但其步伐会比我们过去所习惯的进步速度小得多。比如,芯片设计师找到了使流水线满负荷运转并避免延迟的方法,但是这些措施就像挂在树上的低处的水果一样,早就被人采摘完了。对应用程序的新特性的要求却不会减少,而只会越来越多,适应处理越来越大的应用程序数据量的要求只会越来越迫切。当我们要求应用程序做更多的工作的时候我们就会发现,这已经超出了现有的CPU能力的极限,除非我们将这些应用设计为并行结构的。

有两种思路来处理这种向并行性转化的问题。一个是重新设计你的应用使之适用于并行结构,就像上面所说得那样;还有一种办法相对比较快捷,那就是将代码写得更有效率、更节省资源。这引出了下面这一有趣的话题:

3.              效率和性能优化会变得越来越重要,而不是相反。那些拥有巨大优化能力的编程语言会获得新生,它们不用去处理如何变得更有效率、代码更加优化这一类的问题。可以预期,在相当长的时间内,对于以性能为导向的编程语言的系统的需求会与日俱增。

4.              最后,编程语言和系统会被逼无奈去好好处理并行性这些问题。Java语言从一开始就提供了对并行性的支持,又经过了数个版本来纠正一些错误以保证并行程序设计时的正确性和效率。C++语言很长时间以来就用来书写处理繁重任务的多线程系统,但是其并没有提供对并行程序设计的标准化的支持(ISO C++标准甚至根本就没提到线程这回事,而是故意忽略掉了它),因而导致其并行设计牵扯到了很多不可移植的、平台相关的特性和库。(即使提供了支持也经常是不完全的,比如,静态变量只能初始化一次,因此需要编译器将其用“锁”包装起来,但是很多C++的实现根本就不产生这个“锁”。) 最后,也有不太多的一些并行性标准,比如pthreads OpenMP,和其它的一些现实的或隐式的并行性支持。可以让编译器扫描你的单线程的应用并自动决定如何以隐式的方式将其并行化,这听起来相当的不错,但是这种自动转换工具具有很大的极限性,它并不能得到通过控制你自己的代码,利用显式并行所带来的同等的性能提升。现有的基于“锁”的程序设计的主流技术水平也参差不齐,经常不太稳定或产生错误的结果。与现有的编程语言所提供的能力相比,我对高水平的并行程序设计模型的渴望十分强烈。我很快就会对这方面作更进一步的探讨,当然,这是后话。

结论

如果你仍然没有考虑过并行性的问题,那么现在是时候仔细的检查一边你的代码的时候了,看看那些操作是CPU敏感的(指受限于CPU的计算能力)或将要变成CPU敏感的,并仔细想想这些地方会从并行性这方面得到何种帮助。并且现在也是时候你和你的团队深入理解并行程序设计的要求、陷阱、风格和习惯用语了。

只有非常少的应用是具有天生并行性的,绝大多数则不是。甚至于及时你能准确地找到那些地方是CPU敏感的,你也很难找到将这些操作并行化的方法,所以现在是时候考虑一下这些问题了。隐式并行编译器能提供一些帮助,但是也不要期望太多,它们并不能比手工显式并行化做得更好。你可以手工将你的串行化程序显式并行化,改造成多线程的版本。

拜缓存容量的继续增长所赐,直线型的控制流的优化(指程序的执行会更流畅)仍然会稍稍加强,你还能继续享用一阵子的免费午餐,但是从今天开始,你只能免费享受饭前的entrée(法语,开胃的一些小菜)和饭后dessert(餐后甜点)这点优惠了。精美的肉片(性能提升)依然在菜单上,但是现在要付出额外的代价了---额外的开发工作(effort,指额外的苦力活)、额外的代码复杂性、额外的测试工作(effort)。好消息就是对大多数应用来讲这些额外的努力是很值得的,因为并行性的设计可以使它们最大限度的发挥处理器的呈指数增长的威力。

 
原创粉丝点击