微软C++大师Herb Sutter的文章《The Free Lunch Is Over》翻译,以前自己也经常翻译,但是都不会上传博客。个人很喜欢这篇文章,所以以此作为翻译生涯的开始。
免费的午餐结束了
软件并行计算的基本转折点
继OO之后软件发展的又一重大变革——并行计算
你的免费午餐即将即将结束。我们能做什么?我们又将做什么?
主要的处理器设计生产商,从Intel和AMD到SPARC和PowerPC,已经几乎穷尽了所有的传统方法来提高CPU性能。
他们专注于多线程和多核结构而不再是提高时钟频率以及单指令流性能。这两个特性都已经应用于当今的芯片当中,
特别是,多核已经应用于当今的PowerPC和SPARC4代处理器中,2005年Intel和AMD也将引入这一技术。
事实上,2004年IN-Stat/MDR秋季处理器论坛的主题就是多核,并且许多公司都展示了新型的现代化的多核处理器。
回首往事,从称2004年为多核之年至今也没有很长时间。
多核使我们在软件开发中至少在接下来几年内,在面向通用桌面电脑以及低端服务器应用方面带来重要的转折点(这恰好是当今软件销售的价值体现方面)。
在这篇文章里,我将详细描述硬件的变化、为什么硬件会突然影响软件发展以及并行计算如何影响你和你未来编写程序的方式。
我们可以确信,免费的午餐已经彻底结束一年或者两年了,只是我们才刚刚认识到罢了。
免费的性能午餐
有一个人尽皆知的现象"Intel生产的,Gates都拿走了"。无论处理器运行的多么快,软件都可以找到一种方式耗尽额外的速率。
将CPU提高十倍的速率,软件通常会找到十倍的工作量运行(或者在某些情况写*工作在十分之一的效率下)。
大多数应用在几十年内都可以获得免费规律的性能提升,甚至不需要发行新版本或做其他任何特殊的事情,因为CPU制造商(主要)和硬盘制造商(次要)拥有可靠的更新更快的主流系统。时钟频率不是衡量的唯一标准,甚至不是好性能的必要标准,但却是有指导意义的标准:
我们已经习惯看到500MHz的CPU被1GHz的替代,1GHz的被2GHz替代等等。今天主流电脑的主频可以达到3GHz的范围。
关键问题是:增长什么时候会结束?毕竟,虽然摩尔定律预言会发生指数级增长。但是当我们到达硬件的极限时,显然指数级增长不能永远持续下去。
没有比光更快的事物,增长事实上一定会缓慢下来甚至停止。(注意:摩尔定律主要适用于晶体管集成度,但是在诸如时钟频率等其它领域指数级增长也会发生。
在其他方面甚至增长更快,最引人注意的是数据存储的爆炸性增长,但这一重要趋势适用于不同文章。)
如果你是一个软件开发人员,你可能已经搭上了免费桌面电脑性能的列车。对于某些本地操作你的应用是否已经到达边界?
别担心,传统的智慧离去,明天的处理器将拥有更大的吞吐量。无论如何今天的应用已经日益被诸如CPU吞吐量、存储速率等其他因素所抑制(他们经常与IO绑定,
与网络绑定,与数据库绑定)对吗?
在过去这些一定正确,但是在可预见的将来这一论断将会发生错误。
好消息是处理器将会继续变得强大。坏消息是,至少在短期内,增长的主要方向将不再是迎合当今多数应用的免费习惯之旅。
在过去三十年间,CPU设计师已经在三个主要方面获得了卓越成就,前两个关注与直线流指令执行:
1.时钟频率;
2.执行优化;
3.缓存。
增长的时钟频率意味着需要更多的时钟周期。提高CPU运行速率或多或少意味着做同样工作量会更快。
优化执行流程意味着每个时钟周期做更多事情。今天的CPU运行一些功能更为强大的指令,它们优化的范围可以从外到内,
包括流水、分支预测、同一周期执行多条指令、甚至是重新排序指令执行流程。这些技术都是为了指令流更好或更快地执行,
并且通过减小延迟以及最大化单位时钟工作量来挤压每个时钟周期做更多的事情。
芯片设计者承受越来越大的压力为了加速CPU运行速度,他们讲冒着改变甚至破坏程序意义的风险使它跑的更快。
除了简单的指令排序以及内存模型:请注意,我刚才所说的一些"优化"实际上以及不仅仅是优化了,因为它们可以改变程序的意义
以及引起显而易见的影响,这些已经超出程序员的意料之外。但这却更有意义。CPU设计师一般都是健全和蔼的人们,他们通常不会想要伤害一只苍蝇甚至你的代码。
但是近几年,他们更愿意为了提高每周期运行速率将优化进行到极致,甚至明知这些侵略性的优化会影响到代码的原始语义。这是海德先生出现了吗?根本不是。
这种意愿只是简单地表示芯片设计师在提高CPU性能时所面对的极大压力。他们面对如此重的压力以至于他们为了提高运行速率愿意承担改变代码愿意甚至破坏代码功能
的风险。在读写代码重新排序方面,有两个格外值得注意的例子:允许处理器对写操作进行重新排序后果如此不堪设想以至于这个功能必须被关闭因为当对程序进行任意
的写操作排序后很难了解程序的真正含义。对读操作的重新排序也会带来惊人的明显影响。但是一般它会很有效因为它对程序员而言不难么苦难。并且对于高性能的操作
系统和操作环境的需求为程序员带来极大的负担,因为放弃优化的机会被视为更加愚蠢。最终,设计师将cache与RAM分离并增加它的容量。
主存依旧比CPU慢,因此将需要将它放在离CPU更近的地方,但是却无法比片上缓存更加接近。今天,片上缓存的大小已经飙升,主要的芯片上以2MB或更多的二级缓存芯片
为主。(在这三种史上提高芯片性能的方法中,增加缓存将是唯一将在短期内仍旧持续有效的方法。稍后我会浅析缓存的重要性。)
好了,如上所述,这意味着什么?
首先要承认一件重要的事情就是这一系列事情都与并行计算无关。在这些方面进行加速将直接导致顺序程序(非并行、单线程、单处理器)以及并发程序的加速。
这很重要,因为当今的很多主要程序都是单线程的,这也便于我深刻的开展话题。
当然,编译器也不得不跟上发展水平。有时,为了使用新指令(如MMX、SSE)维护CPU的最优效能你需要重新编译应用。
但是,通常老的应用甚至在没有利用处理器新指令和新特性的前提先,不需要重新编译也可以运行得更快。
世界本事一个好地方,但是不幸的是,它却消失了。
障碍,今天为什么无法生产10GHz的芯片?
图 1 Intel处理器介绍(来自*Intel)
CPU性能的增长早在两年前就已经遇到瓶颈。多数人却在最近才开始注意到。你可以用其他架构的芯片作为对比,这里我仅仅以Intel为例。图一表示历史上Intel处理器的主频以及晶体管集成度的参数。晶体管的集成度至少现在仍然在增长。但是主频则不同。
从2003年开始,你会注意到一个由当前趋势向更快速率发展的拐点。我增加了一条基准线显示最大时钟频率的极限趋势。不再是持续的增长走向,取而代之的是突然的平缓趋势。提高CPU主频变得越来越困难,因为这不仅仅是一个而是几个物理问题造成,特别是热、功耗过大、当前的电流泄露问题。
要点:你的工作站的CPU主频是多少?工作在10GHz吗?对于Intel的芯片,很长时间之前我们就可以达到2GHz(2001年8月),根据2003年的趋势,在2005年初我们就应该研发出10GHz的芯片。其实,我们根本没有做到。我们甚至怀疑是否会看到10GHz芯片的出现。
嗯,那么4GHz呢?我们已经实现3.4GHz了,4GHz还会远吗?唉,事实上4GHz似乎也很遥远。2004年中叶,你可能也知道,Intel首次推迟它的4GHz芯片直到2005年,在2004年秋天,官方正式完全放弃4GHz芯片计划。在撰写本文之际,Intel计划在2005年初可以稍微提高主频到3.75GHz,但至少对于现在来讲,几乎很难实现;Intel和和大多数处理器厂商的未来在于多核这一相同方向。
终有一天我们会在主流的台式机上采用4GHz的芯片,但这一天一定在2005年之后。当然,Intel也拥有在实验室可以更高速运行的样品,但却需要更多的配件,诸如大量的冷却设备。在将来的某一天你的办公室也不会配备那样的设备,或者当你在飞机上使用电脑时无法放置在你的腿上。
摩尔定律和下一代
世界上没有免费的午餐。——R. A. Heinlein
这是否意味着摩尔定律结束了?有趣的是,答案往往是否定的。当然,像指数级增长,摩尔定律终有一天会结束,但这似乎要等到若干年之后。尽管芯片设计师在时钟周期方面已经遇到瓶颈,但是晶体管的数量仍旧日益膨胀,CPU也貌似继续遵循摩尔定律在未来几年日益增加吞吐量。
关键的区别,这也是本文的核心,在于性能的提升在至少下一代代电脑中将以不同的方式实现。并且当前最新的应用也不会再因为没有大规模的重新设计和受益。
在不久的将来,也就是接下来的几年内,新型芯片性能的提升将主要依靠三种主要的方式,只有其中的一种是过去的方式。短期内性能的提升将由下列因素所驱动:
超线程
多核
缓存
超线程是关于在单一处理器中并行地运行两个或更多个线程。超线程在今天已经被应用,同时允许多条指令并行执行。一个限制因素是,超线程需要更多的硬件开销,包括额外的寄存器。它仍然只有一级缓存、一个整数运算单元、一个浮点运算单元等单处理器的特性。超线程有时在合理编写超线程应用时被称为可以提高5%到15%的性能,甚至在理想条件下可以达到40%的性能提高。这已经很好了,但仍然没有提高一倍性能,并且对于单线程应用没有任何用处。
神话与现实:2*3GHz < 6GHz
一个由双核组成的CPU实际上提供了6GHz的处理能力,是吗?
显然不是。甚至在两个处理器上同时运行两个线程也不见得可以获得两倍的性能。相似的,大多数多线程的应用不会比双核处理器的两倍快。他们应该比单核处理器运行的快,但是性能毕竟不是线性增长。
为什么无法做到呢?首先,为了保证缓存一致性以及其他握手协议需要运行时间开销。在今天,双核或者四核机器在多线程应用方面,其性能不见得的是单核机器的两倍或者四倍。这一问题一直伴随CPU发展至今。
其次,除非双核运行不同的进程,或者同一进程的不同线程可以独立运行,并且从来不需要等待其他进程或线程,他们才可能被高效利用。尽管如此,我仍旧可以推测今天运行在双核芯片上的单核应用也可以看到显著的效率提高,不是因为额外的核心实际在做有价值的工作,而是那些可以减慢单核机器运行速度的广告插件和间谍软件。你觉得将其中一个处理器留给间谍软件是解决问题的方案吗?
如果你正在运行一个单线程的应用,那么该应用将仅仅使用一个核心。应该有一定的加速比因为操作系统和应用可以运行在不同的核心。但是操作系统往往不会耗尽一个核心的利用率而导致其中一个核心往往空闲。(同样的,间谍软件业可以占据操作系统的大部分空闲时间。)
多核实际上是指在同一个芯片搭建两个或者更多个核心。一些芯片,包括SPARC和PowerPC,已经拥有成功的多核版本。2005年Intel和AMD的早期产品,功能相似只是集成度略有不同。AMD似乎有一些初步性能设计的优势,比如在同一芯片上具有更好的集成度。而Intel的初始方案几乎是将两个Xeon处理器放置于同一个芯片上。性能的提升应该如同有两个真是的双CPU系统一般(仅仅是系统将更便宜因为母版有两个插槽),这也意味着速度的提升将小于一倍,就像今天这将促进写多线程应用一样,而不再是单线程应用。
最后,片上缓存的大小预计至少在短期内将继续增长。在这三点中,只有这一点将使大多数的现今应用受益。持续增长的片上缓存的大小将对应用程序非常重要且有益处,仅仅是因为空间即速度。内存访问的开销太大,如果有可能你愿意尽量避免访问RAM。在当今的系统,高速缓存未命中而去访问内存的开销一般是访问缓存开销的10倍到50倍。这一点,让人们很惊讶,因为内存访问与外存和网络访问相比要快很多,但是却不能与访问速度最快的缓存相提并论。如果某个应用可以利用缓存的这一特性,那么我们就真正利用了多核,否则我们则没有。这就是为什么在近几年内通过增加缓存大小而不需要大规模重新设计也可以提高很多应用的运行效率。随着越来越多的应用程序处理大量的数据,并为它们增加若干代码以适应新的特性,性能至上的操作需要适应缓存的这一特性。就好像大萧条时期的员工在提醒你,“缓存才是王道”。
(旁白:这是一件最近发生在我的编译器团队身上的事情,恰恰证明了“空间即速度”。编译器使用相同的资源为32位和64位进行编译,程序仅仅是被选择编译成32位还是64位。运行在64位机器上的64位编译器获得了大量的基准性能,原则上是因为64位处理器拥有更多的64位寄存器协同工作以及其他编程特性。一切都很好,但是数据大小呢?升级到64位并没有改变存储器中的大部分数据,只是一部分指针大小变为原来的两倍。当这件事情发生的时候,相对于其他的应用来说,我们的编译器使用了更加繁琐的内部数据结构。因为指针现在是8个字节而不再是4个字节大小,这显然增加了数据大小,在64位工作机制下,我们的编译器在数据大小方面增加显著。更大的工作集几乎抵消了由更快的处理器与更多寄存器协同工作带来的性能提高。在撰写此文之际,64位编译器与32位编译器性能几乎相同。尽管对两者而言资源相同,64位编译器仍旧提供了更大吞吐量。空间即速度。)
但是缓存就是这样。超线程和多核的处理器对当今大部分应用程序则没有影响。
那么,硬件中的这些改变又怎样改变我们编写软件的方式。目前为止你可能已经看到了一些基本的答案,让我们深入研究它以及它带来的影响。
对于软件的意义:下一场革命
在20世纪90年代,我们就学会了面向对象的思想。在过去20年里,也可以说在过去30年间,在编程领域从主流的结构化编程到面向对象发生了巨大的变革。或许还有其他的改变,比如最近兴起的web编程,但是我们大多数在一生中都很难看到对软件革命如此重要和深远的改变。
直到现在。
从今天开始,性能将不会再白白提升。当然,仍旧存在任何人都可以使用的性能提升的途径,这主要归功于高速缓存大小的提升。但是如果你想要你的应用程序从新型处理器的指数级增长的吞吐量中获得提升,就需要精心编写并行通常是多线程的应用。说起来容易做起来难,因为并不是所有的程序在本质上说都是并行的并且并行计算很苦难。我时常可以听到一些*的报道:“并行计算,这并不算是新闻,人们已经开始编写并行计算的应用。”一小部分开发者确实如此。
请记住在20世纪60年代中后期,人们使用Simula语言进行面向对象编程,但是直到20世纪90年代面向对象才在主流编程语言中引发一场革新。为什么?革新的主要原因是我们的产业被编写越来越大的系统、解决越来越多的问题、利用CPU以及存储资源所驱动。面向对象的抽象及独立性使得大型软件的发展更有收益、更可靠及可重用。
同样的,自从黑暗时代我们就开始进行并行编程,编写例程、检测系统以及类似爵士乐的东西。在过去的十年间,我们已经目睹了越来越多的程序员编写多线程、多进程的并行系统。但是这场变革伴随着一个重要的转折点——并行计算的实现变得缓慢。今天绝大多数的应用都是基于单线程的,在下一节我将叙述一些好理由。
顺便说一下,关于炒作的问题:对于他们的新技术人们总是很快宣布这是未来软件发展的革命。不要相信它,新技术往往真正有趣有时有益处,但是这些年中我们从编写软件中获得的巨大革新已经经历了逐步增长过渡到爆炸性增长的阶段。这些是必要的的:你仅仅只能在一个很成熟的技术定义软件发展的变革,包括稳定的供应商和工具支持,至少在七年前没有稳定的性能和致命性缺陷前通常采用各种新的软件技术。作为这样一个结果,真正的如面向对象的软件革新在多年前就已经具有一些优化技术,往往是在十多年前。即使在好莱坞,最真实的“一夜成名”在获得大突破前也需要由很多年的表演经验。
并行计算是编写程序的下一个重大变革。不同专家仍旧对它的影响是否比面向对象更大持有不同看法,当然这样的话题最好还是留给专家。对于技术人员而言,并行计算与面向对象在复杂度和学习曲线上处于同等地位。
并行计算的益处和成本
并行计算,特别是多线程已经在主流软件中被应用有两个重要原因。首先上,逻辑上具有独立的控制流;例如,我设计的一个数据库复制服务程序中,很自然的将每一个复制会话放在自己的线程内实现,因为每个会话完全独立不需要其它会话也可以被激活(只要他们不工作在同一数据库区域)。另一个颇不常见的原因是编写并行程序可以提高性能,要么利用多个处理器的性能或者容易减少程序其他部分的延迟。在我的数据库复制服务程序中,该因素也体现的淋漓尽致,分卡的线程充分利用了多个处理器的性能因为我们的服务器可与其它服务器一起并行计算更多的应用程序。
然而,并行计算的开销却很大。某些明显的成本相对不那么重要。比如,锁的开销很大,但是如果你能找到一种合理地方式并行化操作并且减少或消除共享状态,那么当你使用正确明智时,你会获得很多性能的提升。
可能第二大的成本是并不是所有的应用程序都适合并行化。这点稍后我会进行讨论
最大的成本可能是并行化的使用难度:程序模型,即程序员头脑中的模型,与顺序的控制流相比,程序员需要对使用并行化编程有着合理的理由。
每一个认为自己理解并行计算的人们,最终将发现自己并没有真正理解并行计算。随着程序员学会对可否并行提出疑问,他们发现这些答案通常在内部测试中被发现,他们从而达到了一个新的理解程度。那么通常不会在测试中发现的往往是:理解为什么和怎样做压力测试?这些是否只是停留在多处理系统表面的并行问题?在单处理器系统上线程将在哪些地方进行切换?他们是否真的同步执行而不产生任何错误?这是人们认为他们了解编写并行程序的内心疑惑阶段:我曾遇到过很多团队在压力和扩展测试下应用效果不错,在许多个人网站运行的也很良好,但是直到真的应用到拥有多处理器的环境下,程序出现间歇性的崩溃。
在当今处理器发展前景下,为了在多核机器上运行多线程程序而重新设计编写应用有点儿像通过直接跳入最深的泳池学习游泳,真正的并行环境更容易暴露程序设计中的错误。即使你拥有一个能够可靠编写并行程序的团队,也还会有其他缺陷;比如,并行程序是完全正确的但是没有单核机器运行的迅速,特别是在线程并不能足够独立并且共享程序执行所需的单一资源时。此时,情况变得很微妙。
今天绝大多数程序员都没有真正领会并行计算的真谛,就像15年前大多数程序员还没有领略面向对象的真谛一样。
与从结构化编程到面向对象编程是个飞跃一样(什么是对象?什么是虚拟的功能?我应该如何使用继承?除了“什么”以及“如何”外,为什么正确的设计实践是正确的?),从顺序编程到并行编程同样也是一个飞跃(什么是空转?什么是死锁?它是如何产生,又该如何避免?什么样的结构导致我认为是并行化的程序实际上是串行化。除了“什么”以及“如何”外,为什么正确的设计实践是正确的?)。
今天绝大多数程序员都没有真正领会并行计算的真谛,就像15年前大多数程序员还没有领略面向对象的真谛一样。但是并行化编程是可以学习的,特别是我们坚持以消息和锁为基础进行编程,并且一旦领略它,其实它并不比面向对象困难而且使用起来很自然。你只需要为你和你的团队准备足够的时间进行训练。
(在上面的并行化编程模型中我故意强调基于消息和锁,也有一种无锁编程,至少被Java5和一种流行C++编译器支持。但是对于程序员来说无锁编程要比基于锁的编程难得多。大部分时间里,只有系统和库的编写者不得不需要了解无锁编程,尽管几乎每个人都需要利用系统函数和库函数。坦白讲,即使是基于锁的编程也是有风险的。)
对我们意味着什么
好吧,回到“对我们意味着什么”这一话题。
1.我们已经了解到了明确的主要后果:如果我们想要充分利用以及有效提升的CPU吞吐量,更多的应用程序需要并且在接下来几年会采用并行化编程的方式。例如,Intel正在谈论在将来的某一天研发具有100个核心的芯片,单线程的应用至少可以利用1%的潜在吞吐量。“喔,性能并不那么重要,计算机在变的越来越快”的言论保守怀疑,在不久的将来,这几乎是错误的。
更多的应用需要并行化如果想要完全利用持续指数级增长的CPU吞吐量。效率和性能的提高将会变得更多,而不是更少,这很重要。
现在,并不是所有的应用(或者更精确的说,应用的重要操作)适用于并行化。事实上,一些问题,比如编译,是理想的并行化应用。但是其它的并不是,一个经典的例子是不能仅仅因为一个妇女需要花费9个月时间产下一个婴儿,就意味着九个妇女可以在1个月内产下一个婴儿。在此之前你也可能碰到过类似的情况。但是你有主要到这些现象所引发的问题吗?这些是回答类似问题的答案:你为什么认为这个婴儿问题本质上不能并行化?通常人们错误的引用这些相似的情况证明不能并行化的问题,但事实上没必要正确。如果目标是产下一个婴儿,这事实上是一个非并行化问题。如果目标是产下多个婴儿,这实际上是一个并行化问题。了解实际的目标可以是事情变得大不相同。在软件研发中,当你考虑是否或如何并行化时,你需要谨记这个以目标为基础的准则。
2.可能较不明显的影响是应用与CPU的结合变得越来越紧密。当然,并不是所有的应用都如此,甚至是如果没有准备充分就无法与CPU紧密结合,但是貌似我们已经到达了“应用于I/O结合、与网络结合、与数据库结合”增长趋势的尽头,因为性能在那些领域的提升依然迅速但是传统的CPU性能提升技术却已经过时。考虑下面这一情况:现在我们停滞在3GHz的范围内。因此,对于单线程应用,除非利用增长的高速缓存大小(这是主要的好消息),似乎再也无法提升运行速度。其它的获利似乎比我们过去常常见到的少很多,例如芯片设计师找到新的方式保证流水线满而避免停滞,这一领域收获颇丰。新的应用特性并没有减少,甚至解决大量应用程序数据增长的需要也没有停止。随着我们期待程序可以做更多的程序,除非他们采用并行化编程他们才可以彻底榨干CPU的每一点资源。
有两种方法解决并行化带来的改变。其中之一是如上所述,使用并行化重新设计你的应用程序。另一种相对简单,写更有效的程序浪费更少的资源。这导致了第三种结果:
3. 效率和性能的提高将会变得更多,而不是更少,这很重要。那些具备深刻优化的语言重获新生,他们不需要找到竞争变得更有效、更优化的方式。人们将一直期待以性能为宗旨的语言和系统。
4.最后,程序预压及系统将逐渐*解决并行化问题。尽管为了编写更有效更正确的并行化程序,很多错误在后来的版本才被修正,Java在最初的版本就已经支持并行化。长期以来,C++也被用来编写具备多线程应用的系统,但是一直没有支持并行化的标准(ISO C++标准甚至没有明确的提到线程),并且典型的并行程序需要由具备与平台无关的特性和库文件。(这通常是不完全的,比如,静态变量一定只能被初始化一次,这需要由编译器用锁包括,但是许多编译器并不产生这样的锁。)最终,产生一些并行计算的标准,包括Pthread和OpenMP,一起其他的明确支持并行化的标准。使用编译器检查你的单线程应用程序以判断如何使它并行化是非常好的思路,但是自动的转换工具仍旧有一些限制,并且并不受你自己的代码控制。主流的基于锁的编程艺术,很微妙也很有风险。我们急切需要一个比当今语言更高效的并行化语言模型。一会儿我将谈及这点。
结论
如果你此前没有做过,现在是时候你重新审视你的应用,决定哪些操作时CPU敏感型的以决定在那些地方是否可以使用并行计算。现在对于你和你的团队也是时候领略并行编程的徐需求、诱惑、风格和用法。很少一些应用天生适用于并行化,但大多数并不是这样。甚至当你确切了解到哪些是CPU紧密型时,你也会发现将那些操作并行化非常困难。所有开始学习并行计算的原因是并行计算是软件开发的下一次变革。并行编译器可以起到一些作用,但是不要过分期待。他们永远无法与你通过明确的并行化以及多线程使串行程序并行化的程度相提并论。
感谢高速缓存大小的增长以及指令控制流的优化,免费的午餐还会延续一段时间;但是现在开始提供的东西将会少很多。吞吐量的增长仍旧持续,但是需要额外的研发精力、额外的代码复杂性、额外的测试精力。好消息是很多类应用的额外努力是值得的,因为并行化将使得它们继续利用处理器吞吐量的指数级增长的优势。