免费午餐已经结束

来源:互联网 发布:wind数据库试用版 编辑:程序博客网 时间:2024/06/03 02:24

本文是2005年最重要的技术文章之一,虽然我现在才读到。该文章的译作发表在200611月的程序员杂志上。这是本人翻译的第一篇技术文章,跟大牛的译作比起来还有很长的路要走,但毕竟是一个好的开始,下学期就要开翻译课了,先拿这篇练练手。此译文可读性极差,错误理解当然到处都是,贴出来只是为了鼓励自己,要读的话下面是别人的译作和文章原文的链接。

原文:http://gotw.ca/publications/concurrency-ddj.htm

译文:http://blog.csdn.net/hsutter/archive/2006/08/29/1136281.aspx

 

 

免费午餐已经结束

----软件开发向并发的根本性转变

 

自面向对象的大变革以来,软件开发的又一次巨变即将来临,它就是并发。

免费午餐很快就会结束了。为此你能做些什么,你又正在做些什么?

IntelAMDSparcPowerPC, 主要的处理器生产厂商和架构,已经不能再用传统的方法来提升CPU的性能了。他们不再依靠提高时钟速度和线性指令吞吐量,而是转向超线程和多核架构。现今的芯片已经拥有这些性能,特别地,现今的PowerPCSparc IV处理器已经是多核心处理器,IntelAMD也将在2005年推出多核处理器。的确,2004秋季Stat/MDR处理器论坛上的大主题就是多核设备。许多公司都展现了他们新的或升级的多核处理器。回首展望,把2004年称作“多核之年”并不过分。

       这让软件开发面临着一个根本性的转折点,至少在将来的几年对针对通用桌面计算机和低端服务器的应用程序来说是这样(而这些软件正是现今软件利润来源的主体)。在这篇文章里,我将讲述硬件正在改变的一面,为什么它突然对软件开发产生了影响,以及并发的变革对你来说具体意味着什么,它将可能怎样改变你将来编写软件的方式。

       事实上,免费的午餐已经结束一两年了,只是我们现在才发觉。

免费的性能午餐

有一个有趣的现象就是, 英特尔付出,微软取走。不管处理器多快,软件会不断找到新的方式消耗掉额外的速度。让处理器快十倍,软件也会找到十倍的事情去做(或者,有时我们愿意降低十倍效率去做事情)。几十年来多数软件没有通过发布新版本或创新就享受着免费和经常性的性能提升,这是因为处理器厂商(主要)以及内存和硬盘厂商(次要)不断提供更新更快的主流系统。时钟速度并不是衡量性能的唯一标准,甚至不是一个好的衡量标准,但是却是有启发性的:我们曾经看到1GHzCPU取代了500MHzCPU,然后又被2GHzCPU取代。现在我们的主流系统都在3GHz左右。

       关键问题是:它什么时候结束?毕竟摩尔定律预言的是指数增长,很明显在我们到达物理极限之前指数增长不会一直继续下去,像光不能变得更快一样。这个增长过程最终必须减慢甚至停止。(注意:当然摩尔定律主要适用于晶体管密度,但是同样的指数增长也发生在一些相关的领域比如时钟速度。在其他领域甚至有更快的增长,最明显的例子就是数据存储的爆炸性增长,但是那种重要的增长趋势就超出了本文范畴了)

       如果你是一个软件开发者,那么你可能已经在享受桌面计算机性能提升的免费午餐了。你的应用程序的性能受局部操作的限制吗?不用担心,传统的规律依然有效,以后的处理器会有更多的吞吐量。总之,应用程序的瓶颈不在CPU的吞吐量和内存速度而是其他一些因素比如I/O受限、网络受限、数据库受限,对吗?

在过去是很对的,但是在可预见的将来是绝对错误的。

       值得高兴的是处理器会不断变得更强大,但不幸的是至少在短期内,增长的方式不会让现在的多数程序继续享受传统的免费性能提升。

在过去的三十年中,CPU的设计者在三个主要的领域获得了性能的提升,前两者关注直线执行流。

时钟速度

执行优化

缓存

       提高时钟速度就是获取更多的周期。CPU变快或多或少意味着以更快的速度完成相同的事情。

优化执行流就是在每一周期完成更多的工作。今天的CPU支持许多强大的指令,从常见的到奇异的指令都会进行优化,包括流水线操作,分支预测,在同一时钟周期执行多条指令,甚至通过记录执行流来打乱顺序执行。设计这些技术是为了让指令流动性更好或执行更快,通过减少反应时间和最大化每个时钟周期的工作量来在每一时钟周期完成更多的工作。

简单讲一下指令的重排序和内存模型:注意我刚才提到的一些优化远不是优化,因为它们会改变程序的意图,打破了程序员合理假设。这一点很重要。CPU的设计者一般都是理智的家伙,他们正常情况下不会伤害苍蝇,更不会想到去破坏你的代码,我指的是正常情况下。但是在最近几年,他们为了从一个时钟周期中挤出更多的速度而追求大胆的优化。他们甚至很清楚这种冒险的重新安排会影响你的代码的语义。这种意愿清楚地表明芯片设计者为了推出更快的CPU而面临着巨大的压力,他们面临如此大的压力以至于为了让程序运行的更快,不惜冒险改变你的程序的意图甚至破坏它。在这方面两个值得注意的例子是:写重排序和读重排序。允许处理器重排序写操作有着令人惊讶的后果,这样会出乎程序员的预料。这种功能应该关闭,因为如果有任意的写操作重排序,程序员将很难弄明白自己程序的意图。读操作重排序也会产生惊人的可见效果,但是这个功能可以打开因为这对程序员来说并不是特别难。对性能的需求导致操作系统和操作环境的设计者妥协选择了一种加重程序员负担的模型,因为这样被认为比放弃优化的机会罪过轻一些。

最后,增加片上缓存的大小意味着远离内存。由于主存依然比CPU慢得多,让数据更靠近处理器是合理的,而没有比直接放在芯片上更靠近CPU的了。片上缓存的大小不断剧增,现在主要的芯片卖主会卖给你带2MB或更多片上二级缓存的处理器。(在过去的提高CPU性能的三个主要方法中,增加缓存是近期内仍会有效的仅有的一个,后面我还会提到缓存的重要性。)

这意味着什么?

       关于这些领域的一个基本的重要认识就是它们都是并发无关的。在这些领域的加速会直接导致线性(非并行、单线程、单进程)应用程序的加速,也包括应用并发的程序。这很重要,因为现今主要的应用程序都是单线程的,下面我会详细解释原因。

       当然编译器应该赶上,为了利用CPU的新特性或从新指令集(比如MMX,SSE)中受益,你需要重新编译你的程序。但是基本上老的程序总是能运行的明显加快,甚至没有利用最近的CPU提供的新指令和新特性。

       那样的世界确实是个美好的地方。但不幸的是,它已经消失了。

为什么我们今天没有10GHz的芯片

我们知道CPU性能提升已在两年前遇到阻碍。许多人现在才开始察觉。

下面是Intel的数据,用其他芯片你会得到相同的结果。图1是用时钟速度和晶体管数量描绘的Intel芯片的发展历史。至少是现在,晶体管的数量一直在增长。但是时钟速度却是另一回事。

 

2003年开始左右,你会发觉以前CPU时钟速度快速增长的趋势突然转变。我添加了一条线来表示时钟速度的极限,通过细点线你可以看到有一个突然的平缓而不是沿着以前的路线增长。因为不只一个的物理原因,获取更高的时钟速度变得越来越难,主要是热量(太多以至于难于驱散),能量消耗(太高)和漏电问题。

你现在的工作站上的CPU时钟速度是多少?有10GHz吗?在Intel的芯片上,我们在很早以前就达到了2GHz(20018)。根据2003年前的CPU增长趋势,2005年我们应该拥有10GHz的奔腾家族芯片。而事实表明我们并没有。另外,这种芯片甚至是不可预见的,我们不知道到底它什么时候会出现。

       那么4GHz呢? 我们已经有3.4GHz了,当然离4GHz应该不远了,对吗?唉,甚至4GHz实际上都很遥远。你可能知道在2004年中,Intel把它生产4GHz的计划推迟到2005年,而在2004年秋季正式取消了这一计划。写这篇文章的时候,Intel正打算在2005年初稍微提升一点到3.73GHz (见图1最右边的点),但是至少现在时钟速度的提升真正结束了。芯片公司正大胆追求新的多核方向,Intel和其他处理器厂商的未来在别的领域。

摩尔定律和下一代处理器

世界上没有免费的午餐.” —R. A. Heinlein, 月亮是个无情的情人》

这意味着摩尔定律结束了吗?很有趣的是答案基本上是否定的。当然像所有的指数进程一样,摩尔定律某一天肯定会结束,但是在最近几年不会有这种危险。尽管芯片工程师在提高时钟速度上遇到了困难,但是晶体管数量依然会继续增长,似乎在将来的几年内CPU依然会按摩尔定律提高吞吐量。

关键的区别,也正是这篇文章的核心就是,至少以后几代CPU性能的提升都有通过与过去根本不同的方式达到。现今大多数应用程序如果不重新设计的话,将不会再享受免费的性能提升。

       在不久的将来,也就是以后的这几年,芯片性能会通过三个主要的途径获得提升,其中只有一个与过去相同。这些性能提升的因素有:

超线程

多核

缓存

超线程是指在单个CPU内并行运两个或多个线程。

       超线程CPU已经存在,它们允许多条指令并行运行。但是一个限制因素是,虽然一个超线程的CPU有一些额外的硬件包括额外的寄存器,但是它仍然只有一个缓存,一个整数运算单元,一个浮点运算单元,基本上只有一个CPU的大多数基本功能。超线程优势被引证为能为良好编写的多线程程序提供5%15%的性能提升,在理想情况下甚至可以为精心编写的多线程程序提供40%的性能提升。这很不错,但是却很难让性能加倍,而且它不会对单线程程序起作用。

       多核是指在片上运行两个或多个CPU。许多芯片包括SparcPowerPC已经有多核版本。IntelAMD最初的设计都定在2005年,它们虽然集成度不同但是功能却类似。AMD最初的实际似乎有一些性能设计的优势,比如在同一硬模上对支持的功能进行更好的集成。而Intel最初的设计基本上是把两个Xeons粘到同一硬模上。性能的提升最初应该与一个真正的双CPU系统类似,只是这样的系统会便宜一些,因为主板不需两个CPU插槽和附属的黏合工具。这意味着即使在最理想的情况下速度也不会加倍,它只能适当提升良好编写的多线程程序。当然不包括单线程程序。

最后,片上缓存的大小可以期待会继续增长,至少在短期内是这样。在这三个领域中,只有这个会让多数现存的程序广泛受益。片上缓存大小的持续增长非常重要并且会使许多程序受益,仅仅因为空间就是速度。访问主存的代价是昂贵的,如果有办法的话你绝对不会考虑内存。在现在的系统上,从主存上取信息要比直接从缓存上取要慢1050倍,这一直让人们很惊讶,因为人们认为内存很快。内存相对与硬盘和网络是很快,但是远比不上片上高速运行的缓存。如果程序工作集能放进缓存里,对我们再好不过。这就是为什么增加缓存容量会挽救一些现存程序,不必重新设计就可以让它们重获生机。当已有程序要处理越来越多的数据,随着他们不断升级添加新的功能,性能敏感的操作需要继续放进缓存。一个萧条年代的老前辈会迅速提醒你:“缓存才是王道”。

       (附注:下面是发生在我们编译器团队里的轶事以证明“空间就是速度”。32位和64位编译器用的是相同的源码库,代码只是被编译为32位或64位进程。 64位编译器在64位的CPU上会取得更好的基本性能,主要是因为64位的CPU拥有更多的寄存器以及其他改善代码性能的功能。这样很好,但是数据呢?转到64位不会改变内存中多数数据的大小,当然指针是原来的两倍大小。我们的编译器使用指针作为其内部数据结构比任何其他应用程序都要多得多。因为指针现在变成了8个字节而不是4个字节,这导致了数据量增加。在64位的编译器上我们看到工作集明显增大。更大的工作集导致性能受损,几乎正好和我们从带有更多寄存器的更快的处理器上获得的代码执行性能提升相抵消。写这篇文章的时候,64位的编译器和32位编译器运行的一样快,尽管它们用的代码库相同而64位处理器提供更好的处理吞吐量。)

虽然缓存是这样,但是超线程和多核CPU机会不会影响多数现在的程序。

       硬件的这些改变对我们编写软件的方式意味着什么?现在你或许已经有一个基本的答案了,那么下面我们来研究一下它和它的重要地位。

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

20世纪90年代,我们学习体验对象这个概念。主流软件开发从结构化编程到面向对象编程的大变革无疑是过去20年甚至是30年以来最伟大的改变。当然还有其他的变化,比如最近web services的诞生,但是我们大多数人在我们的职业生涯中没有看到像面向对象革命这样对我们编写软件的方式产生如此根本和深远的影响。

直到现在是如此。

从今天开始,性能午餐不再免费了。当然基本上我们依然会享受可用的性能提升,这要归功于缓存容量的提升。但是如果你希望你的程序能从新处理器指数吞吐量增长中获取优势,它就必须是良好编写的并发(主要是多线程)程序。说比做容易,因为并不是所有的问题在本质上都是可以并行化的,另外并发编程也难于掌握。

我听到了抗议的声音:“并发?这早已不是新闻!人们已经在编写并发程序了。”这是事实,不过只有为数不多的开发者。

       记得吗?至少从1960年代末Simula的时代开始人们就在做面向对象的编程。但是直到1990年代,OO才成为变革,在主流开发中占据了统治地位。那么为什么呢?变革的主要原因是我们的产业需要编写越来越大的系统来解决越来越复杂的问题,并充分利用越来越好的CPU和存储资源。OOP在抽象和管理依赖关系上的优势让它在开发经济、可靠、可重用的大规模软件上成为必需。

       同样,我们从那些黑暗的时代开始就在编写并发程序,写一些协同程序、监视程序和一些类似的好东西。在过去的十年左右我们也见证了越来越多的程序员开始编写并发(多线程、多进程)系统。但是由转向并发的转折点标志的真正革命却实现地缓慢。而现今大多数的程序都是单线程的。我将会在下一节概述并发编程。

顺带提一下关于宣传这件事,人们总是喜欢宣称所谓的“下一代软件开发革命”,通常是对他们自己的新技术。千万不要相信他们。新技术往往很有趣,有时也很有益处,但是真正影响我们编写软件方式的大革命往往是那些已经存在了很久的技术,他们在突然爆发式增长之前都经理了循序渐进式的增长。软件开发变革只能基于已经足够成熟的技术(包括拥有硬件厂商和开发工具的支持)。这是必须的。新技术一般需要至少七年的时间才能足够成熟以至广泛应用,而不会产生性能障碍和其他问题。因此,像OO这样的真正的软件开发革命只能发生在那些酝酿了几年甚至几十年的技术上。就是在好莱坞,多数真正的“一夜成名”在他们真正成名之前已经奋斗了好多年。

       并发是下一个影响我们编写软件的主要变革。至于它是否比OO影响更大,专家们有着不同的观点。但是这样的讨论还是留给那些博学多才的人们吧。对于技术人员来说,有趣的是据估计并发和OO在变革规模、技术复杂度和学习曲线上极为相似。

并发的好处和代价

并发(特别是多线程)已经应用在主流软件中有两个主要原因。一个是逻辑分开自然独立的控制流。比如,在一个数据库复制服务器上,把每个复制任务分到各自的线程之中是很自然的设计,因为每个任务跟其他活动的任务是完全独立的(只要他们不是在同时操作数据库的同一行数据)。另一个不是很普遍的原因是,在过去编写并发的代码是为了性能,去充分利用多个物理CPU或者利用程序其它部分的潜能。在我的数据库复制服务器上,这个因素跟独立的线程一样能够在多CPU的系统上很好地提高效率,跟其他服务器协同处理越来越多的并发复制任务。

但是并发也有实际的代价。一些明显的代价实际上相对来说并不重要。比如,获取“锁”的代价会变高。 但是如果你找到明智的方式去并行操作和减少或消除共享状态,你从并发中获取的要比你在同步上失去的要多得多。

       或许并发的第二大代价就是并不是所有的程序都能够并行。我一会会解释它。

       或许并发最大的代价就是并发难于掌握。关键是程序员头脑中并发的编程模型。程序员需要对他的程序有可靠的推理,这比顺序控制流要难得多。

       每个学过并发的人都觉得他们懂并发,但最终都以他们以为不可能的神秘冲突结束。最后他们发现他们根本不懂什么是并发。当开发者学习构思并发程序的时候,他们发现那些冲突可以被合理的内部测试捕获。他们因到达了一个新的知识层面而备受安慰。但是测试不能捕获的是那些只会发生在真正的多处理器系统上的潜在并发缺陷。(除了那些懂得为什么以及怎样进行真正的压力测试的实验室。)在多处理器系统上程序是真正同步执行的,而不是像单处理器那样通过切换线程实现,从而导致了新的一类错误。这是对那些认为自己真正懂并发的人的又一打击。我遇到过许多团队编写的程序通过了重负荷和扩展的压力测试,在许多客户站点上也运行良好。但当用户真正拥有多处理器机器的时候,神秘的冲突和崩溃又会间歇性地出现。在现在的情况下,重新设计你的程序去在多核机器上跑多线程就像跳进深海游泳一样。进入到真正并行的环境之中将你的错误都暴露无遗。即使你的团队可以编写可靠安全的并发代码,仍然会有别的问题。比如,完全安全的并发代码可能并不比在单核机器上快多少。因为线程并不是足够独立,而是依赖共享一个单一的资源,这样实际上又将程序的执行顺序化了。这些东西确实微妙。

就像结构化的程序员学习OO的突破一样(什么是对象?什么是虚函数?我怎样使用继承?除了这些“是什么”和“怎样”之外,为什么正确的设计实践确实可靠?),顺序程序员学习并发也要经历这个过程(什么是冲突?什么是死锁?它是怎样产生的,怎样可以避免?什么结构让我以为并行的程序顺序化了?怎样让消息队列成为我的朋友?除了这些“是什么”和“怎样”之外,为什么正确的设计实践确实可靠?)。

       大多数的程序员并没有掌握并发,就像15年前大多数程序员没有掌握对象一样。但是并发变成模型是可以学习的,特别地如果我们抓住基于消息和锁的变成。一旦掌握了并不比OO难多少,也会变得一样自然。只要准备好,允许为你和你的团队投入培训和时间。

       (我故意将上面的描述限制在基于消息和锁的并发编程模型。其实还有无锁编程,在Java5语言级别和最近的一种流行的C++编译器上得到了直接支持。但是并发无锁编程对程序员来说比基于锁的并发编程要难于理解和构思。多数情况下,只有系统和库的编写者需要理解无锁编程,尽管事实上每个人都应该利用这些人创造的无锁系统和库。坦白地讲,甚至是基于锁的编程都是危险的。

它对我们意味着什么?

       好的,现在我们讲讲他到底对我们意味着什么?

       1.我们已经提到的一个很明显的重要后果就是,如果程序想充分利用CPU吞吐量的增加,就会日益需要应用并发。比如,Intel说他们某一天会生产100个核的芯片。单线程才程序只能利用这样的芯片吞吐量潜能的百分之一。“性能并不重要,计算机会变得越来越快”,这一直就是一个很幼稚的想法,甚至在不久的将来它将是错误的。

       现在,并不是所有的程序(更精确地说是程序的重要操作)都能够并行化。的确,一些问题,比如编译,几乎是完全并行的。但是其他的问题并不是这样,这里通常的反例就是,一个女人九个月生一个孩子并不意味着九个女人一个月可以生一个孩子。你以前可能遇到过类似的比喻。你能发现比喻中所蕴含的问题吗?下一次有人再用这个比喻问你的时候,你可以用下面的问题反问:你能从比喻中得出人类生产婴儿的问题根本上是不能并行的吗?通常人们用这个错误的比喻去证明一个本质上不能并行的问题,但是这实际上并不正确。如果目标只是生产一个小孩的化,这个问题确实本质上是不能并行的。但如果目标是生产许多小孩的话,这个问题就是一个很理想的并行问题。知道问题的真正目标改变了一切。在构思是否可以或怎样并行化软件的时候,在头脑中要清楚基本的面向目标的原则。

2. 或许一个不太明显的结果就是程序可能变得愈加受CPU限制。当然,并不是所有的程序操作都会CPU受限,甚至那些将要受到影响的软件也不会突然变为CPU受限,如果它们以前并不是这样。但是我们似乎看到 “程序日益I/O受限或网络受限或数据库受限”这种潮流的结束,因为这些领域的性能仍在不断的快速提升(GBWiFi, 或者其他?),而传统的CPU性能提升技术已经走到尽头。考虑一下:我们在3GHz左右停住了脚步。因此除了缓存容量的还会不断增加这个好消息之外,单线程的程序不会运行的更快。其他方面的进展可能比我们以前看到的要缓慢,比如芯片设计者找到新的方式让流水线繁忙避免停滞。这些本来就收效甚少的领域已经充分开发过了。然而对软件新性能的需求不会减少,处理大量程序数据的趋势不会停止增长。当我们不断要求程序去做更多的时候,我们就必须去编写并发代码,否则我们的CPU会耗尽。

有两种方式可以应对程序转向并发的突然转变。一个是,像上面提的那样,用并发重新设计你的程序。另一个简朴的方法就是,编写高效的代码减少浪费。这导致了第三个有趣的结果。

3. 效率和性能优化将变得更加重要。那些已经充分利用优化的语言会找到新生,那些没有利用的会找到方式去竞争,变得更高效和可优化。预计对面向性能的语言和系统的长期需求会日益增加。

       4.最后,编程语言和系统将日益被迫处理并发。Java语言从一开始就包含了对并发的支持。为了让并发程序更加正确和高效,Java语言犯了许多错误并在后续的很多版本中得以改正。C++语言一直用来编写良好的重型的多线程系统,但是它根本没有对并行的标准化支持(ISO C++标准根本没有提及线程,这也是故意之举)。所以并行基本上都是通过利用平台相关不可移植的并行功能和库来实现的。(而且这也并不完备,比如,静态变量只能初始化一次,这通常需要编译器对静态变量加锁,但是许多C++实现根本不会生成锁。)最后,几个并行的标准包括pthreadsOpenMP支持显式的或隐式的并行化。让编译器分析你的单线程程序然后自动把它并行化再好不过,但是这种自动转换工具是有限的,并不能产生你自己用代码控制并行的效果。这种精妙而又危险的艺术的主流状态就是围绕基于锁的编程的。我们迫切需要一个支持并发的高级编程模型,而现在的编程语言又不能提供。我一会还要讲到这些。

结论

如果你还没这样做的话,现在是你该好好看一下你的程序设计的时候了。看一看那些操作是或者将要是CPU敏感的,找出你能从并发编程中受益的地方。现在也是你和你的团队该学习一下并发编程的需求、缺陷、样式和习惯的时候了。

       很少的几类程序是固有可以并行化的,但是大多数程序不是。甚至到你确切知道哪里是CPU受限的,你看能很难去弄明白怎样并行这些操作。这就是我们必须现在就要开始思考的原因。编译器隐式的并行化可以帮助我们一点,但是不要期待太多。你把程序变成并行和线程化的版本所达到的成效,是编译器隐式并行化你的顺序程序所不能达到的。

       因为缓存的继续增长和更好的线性控制流优化的存在,免费午餐还会持续一段时间。但是从今天开始餐厅只会提供一道主菜和一道甜点。吞吐量增加的可口小鱼依然会在你的菜单上,但是会有额外的费用,额外的开发付出,额外的代码复杂度,额外的测试付出。好消息是对许多类程序来说这种额外的付出是值得的,因为并发会让它们充分利用处理器吞吐量的指数增长。

 

原创粉丝点击