引言
本文档的目的是为了同大家分享多年来我在开发一种特定类型的应用时形成的一些观点,而“服务器”只是对这类应用程序的一个不是那么恰如其分的称谓。更准确的说,我将描述的是一大类的程序,这类程序的设计使得它们能够在每秒钟内处理数量十分巨大的离散消息或请求。网络服务器是最为常见的同此定义吻合的软件,但是,并非所有同此定义吻合的程序绝对可以称作是服务器。然而,“高性能请求处理程序”这种称谓又很难让人接受,所以,为了行文简单起见,我就用“服务器”这个词了事了。
尽管在单个程序中进行多任务处理现在早已司空见惯了,但我将不会对“适度并行”的应用程序进行讨论。就在现在,你阅读本文档所用的浏览器可能正在以并行的方式做着一些事情,但是如此低水平的并行真是不会带来任何值得关注的挑战。真正值得关注的挑战出现在处理请求的架构本身是总体性能的限制性因素的时候,此时对架构进行改善就能够真正地提高性能。运行在主频为几G赫兹的CPU和上G内存的环境中,通过DSL线路同时进行着6个下载任务的浏览器,往往就不属于这种情况。这里的重点并不在于象是用吸管喝着饮料似的应用程序,而是在于象是通过消防拴来喝水的应用程序,这类程序处于马上就要突破硬件能力的边缘地带,你对这类程序的设计起着至关重要的作用。
毫无疑问有些人会反对我的意见和建议,或者认为他们有更好的办法。这非常好。这里我可不是想要发出什么上帝之声;这些只是我发现正合我意的方法,但合意的标准并不仅是它们在性能方面的表现不错,而且还包括后期对这些代码进行调试和扩展的难度也不高这个标准。你的衡量标准可能有所不同。如果你发现有别的方法更适合你那就太棒了,但是要警告的是,我在本文中建议的作为替代方案的几乎所有方法我都试过了,而且其结果都很令人气恼和不可接受。你所钟爱的观点要是放到本文中作为其中的故事之一可能也会非常的合适,如果你怂恿我把这些故事写出来,无辜的读者可能会被我烦死的。你可不想伤害读者,对吧?
本文剩下的部分将围绕着我称之为“性能低下的四骑士”的四个方面的内容来进行:
数据拷贝
上下文切换
内存分配
锁的争用
本文最后还包含了一个包罗万象的部分,但是这四个是最大的性能杀手。如果你能在不拷贝数据、无需上下文切换、不用进行内存分配而且不会引起对锁的争用的情况下处理绝大多数请求,那么你的服务器性能一定会非常好,即使有些小地方做得不对也没有太大的关系。
数据拷贝
因为一个非常简单的原因,这一小节本来可以写得非常简短:绝大多数人都已经有过这方面的教训了。每个人都知道,数据拷贝很不好;这很显然,对吧?嗯,真地很对,正是因为你在你的计算领域生涯的早期就有过这方面的教训了所以很显然,并且之所以你有这方面教训是因为早在几十年前就有人开始提出数据拷贝这个词了。我知道我的情况就是这样的,但我有点跑题了。现如今,在每个学校的课程和各种非正规的指南中都会对数据拷贝进行讨论。即使那些做销售的都已经弄明白了,“零拷贝”是个不错的时髦词。
尽管后知后觉显然认为数据拷贝很不好,但是,貌似人们还是没有弄明白其中的一些细微之处。其中最重要的一点就是,数据拷贝往往发生地很隐蔽,形式上也有所伪装。你真的了解你所调用的驱动程序或者代码库里面到底有没有进行数据拷贝吗?可能情况比你所想的要复杂一些。请你猜猜看PC中“程序控制的I/O”指的是什么。哈希函数就是一个不是隐蔽的而是经过伪装的数据拷贝的例子,它具有数据拷贝的所有内存访问开销,而且还涉及了大量的计算过程。 只要指出来哈希实际上是个“数据拷贝再加其它操作“的过程,那么貌似有一点很显然,就是要避免使用哈希函数了,但是,我知道至少有一群高人会把这个问题解决掉。如果你真想消除数据拷贝,不管是因为它们真地会损害性能,还是因为你就是想把“零拷贝操作”写入你在黑客大会上的幻灯片里,你将需要对很多并不没有大张旗鼓告诉你但其实真的包含了数据拷贝的很多东西一直追查到底。
经实践验证过的避免数据拷贝的方法就是使用间接法,传递缓冲区描述符(或者是一个缓冲区描述符组成的链)而不是仅仅传递缓冲区指针。每个描述符一般都由以的几个部分组成:
●一个指针以及整个缓冲区的长度。
●一个指针和长度,或者是缓冲区中真正填充了数据部分的。
●指向列表中其它缓冲区描述符的前向和后向指针。
●一个引用计数
现在,不用通过拷贝一段数据来确保这些数据能够呆在内存中了,代码可以很简单地对适当的缓冲区描述符中的引用计数加一。在某些情况下这种做法会相当地成功,包括在典型的网络协议栈的运作模式中也没问题,但是,这种做法也有可能会成为一件让你大为头疼的事情。一般来说,要在缓冲区描述符链的开头或者结尾部分添加新的缓冲区很容易,同样为整个缓冲区增加引用以及立即撤销为整个链分配的内存也很容易。在中间部分添加新缓冲区、一点一点的撤销已分配的内存或者引用部分缓冲区这三种操作每一个都会让你的日子越来越难过。要想对缓冲区进行分割或者合并只会把你逼疯。
然而,实际上我并不建议在所有情况下都采用这种方法。为什么不建议呢?因为采用这种方法后,每次想查看报头部分的时候你都不得不对描述符链进行遍历,这么做真是太痛苦了。这里真的还有比数据拷贝更加糟糕的事情。我发现,要做的最好的事情就是找出程序里的大对象,比如数据块,确保象前文所述那样,为这些数据块独立分配内存,这样就不需要对它们进行拷贝了,至于剩下其它的东西就不要操那么多心了。
这就是我对数据拷贝要说一下我的最后一个观点:在避免数据拷贝时不要做得太过火。我看到过太多代码,为了避免数据拷贝而它们把某些事情搞得更糟了,比如,它们会迫使系统进行上下文切换或者会打断数据规模较大的I/O请求。数据拷贝代价比较高,当你正在寻找需要避免冗余操作的地方时,其中首要的就是应该看看有没有出现数据拷贝的地方。但是,有一点会减少这么做对你的回报。仔细排查代码,然后就是为了排除掉最后的几个数据拷贝而把代码搞到复杂了两倍多,这通常是对时间的一种浪费,这些时间本可以更好地花在别的地方。
上下文切换
鉴于每个人都认为数据拷贝显然不好,我经常惊叹于竟然有那么多人会完全忽略上下文切换对性能的影响。按照我的经验来看,在高负载的情况下,同数据拷贝相比,实际上上下文切换实际才是更多的导致系统“完全失灵”的元凶;系统开始在来回从一个线程到另一个线程的切换中所花的时间比线程真正做有用的工作所花的时间还要多。令人惊奇的是,从某个角度讲,引起系统过度进行上下文切换的元凶十分显而易见。上下文切换的头号原因就是活跃线程数超过了处理器的总数。随着活跃线程数同处理器总数比值的增大,上下文切换的数量也会增大。如果幸运,这种增加会是线性的,但通常都是成指数级增长。这个非常简单事实可以解释出每个连接都用一个线程来处理的多线程设计为什么伸缩性会非常之差。可伸缩系统唯一比较现实的方案就是限制活跃线程的总数,让该数(在一般情况下)小于或等于处理器的总数。这种方案有一种比较多见的经过修改的版本就是只使用一个线程;尽管这么做的确能够完全避免上下文胡乱切换,而且还不用再使用锁了,但它也无法利用多CPU来提高总吞吐量了,所以除非所设计的程序是非CPU密集型的(通常是网络I/O密集型的),一般大家都不采用这种方案。
一个“适度使用线程”的程序要做的第一件事就是找出如何让一个线程同时处理多个连接的办法。这通常意味着要在前台使用select/poll API、异步I/O、信号或者完成端口,而后端使用一个事件驱动的结构。关于到底哪种前台API才是最好的,已经发生过许多类似于“宗教之争”的争论,而且这种争论还会持续下去。Dan Kegel所写的C10K论文是这个领域中最好的参考资料。我个人认为,各种select/poll API和信号都是些丑陋的伎俩,因此我比较偏爱AIO或者完成端口,但这实际上并没有那么重要。所以这些方案,也许要将除select()外,用起来都相当不错,真地都不会做太多的事情来解决发生在你的程序前端最外层之外的任何问题。
事件驱动的多线程服务器最简单的概念模型以队列为中心;一个或多个“监听者”线程读取请求并将其放入队列之中,然后由一个或多个“工作者”线程将请求从队列中取出并对它们进行处理。从概念上讲,这是个好模型,但太多人真的就按照这种方式来编码了。为什么这么做不对?因为导致上下文切换的第二号原因就是将工作从一个线程传递给另外一个线程。有些人甚至会让原先的线程来发送对请求的响应,这样势必造成处理每个请求时不是发生一次而是两次上下文切换,这可真是错上加错啊。这里很重要的一点就是,要采用一种“对称”的方法,一个给定的线程可以在根本不引起上下文切换的情况下,可以在刚开始时是监听者的身份,随后其身份可以变换为工作者,然后再次成为监听者。这种方法到底需要在线程间分配所有的连接还是需要让所有的线程按次序排队成为所有连接的监听者,似乎并不太重要了。
通常即使对于将来的下一刻,也很难知道系统中到底有多少活跃的线程。毕竟请求可能会在任何时刻从任意一个连接中发过来,还有专用于处理各种维护任务的“背景”线程也可能会挑选在那个时刻醒过来。如果你不知道到底有多少个线程是活跃的,那你怎么才能做到限制系统中应该有多少个活跃线程呢?从我的经验来看,最简单同时也是最有效的方法之一就是:采用一个老式的计数信号量,当每个线程在做“真正的工作”时,它必须持有该信号量。如果活跃线程数已经达到上限,那么处于侦听模式的每个线程在醒来的时候,可能会导致一次额外的线程切换,随后就会阻塞在该信号量之上,但是一旦所有侦听模式的线程都以这种方式进入阻塞状态,那么直到现有线程之一“退出活跃状态”之前,它们就不会再对系统资源进行争用了,因此它们对系统性能的影响可以忽略不计。更重要的是,这种方法还处理了维护线程,这些线程在大多数的时间中都处于休眠模式,所以不会计入活动线程计数,这种处理方式比其它的方案要更加的优雅。
既然将请求的处理过程分为了两个阶段(监听者和工作者)并由多个线程来为这两个阶段服务,那么将处理过程更进一步分为多于两个的阶段就是很自然的事情了。按照最简单的形式,请求的处理就变成了先在一个方向完成一个阶段的处理过程,然后再在另外一个方向上(为了响应请求)进行另外一个阶段的处理。然而,死去可能会变得更加复杂;有一个阶段可能会代表着在涉及不同阶段的两个处理路径上进行“分叉”,或者该阶段可能会产生一个响应(比如,该响应是个缓存中的值)而无需进行下一个阶段的处理了。因此,每个阶段都需要能够为请求指定“下个阶段应该干什么了”。这里有三种可能,由每个阶段的分发函数的返回值来表示:
●该请求需要接着传递到另外一个阶段(在返回值里用一个ID或指针来表示这个阶段)。
●该请求已经处理完毕(用一个专门的“请求处理完毕”返回值来表示)。
●该请求被阻塞(用一个专门的“请求被阻塞”返回值来表示)。这等价于一种情况,只是该请求仍未释放,随后会在另外一个线程中接着对其进行处理。
请注意,在本模型中,请求的队列操作是在阶段 中完成的,而不是在阶段间完成的。这样就能够避免常见的愚蠢做法:不断将请求放入后继阶段的队列之中,然后立即进行该后继阶段并将该请求从队列中取出。我认为,类似这样的队列活动以及加解锁的动作绝对是无事生非。
把一个复杂的认为分割成相互通信的多个较小部分的这种做法如果感觉很熟悉的话,那是因为这种做法已由来已久。我的方法的根源是1978年由C.A.R. Hoar阐明的概念通信顺序进程(Communicating Sequential Process,简称CSP),而CSP又是基于早在1963年,也就是在我出生之前,由Per Brinch Hansen和Matthew Conway提出的一些观点。然而,在当初Hoare创造CSP这个名词时,他所说的“进程”是在抽象的数学意义上讲的进程,CSP进程跟操作系统中那个具有相同名字的实体并无关联。使用运行在单个OS线程中的、跟线程看上去很象的协程(coroutine)是实现CSP的最常见方法,而且依我看,就是这种实现方法给用户造成了这样的棘手的局面:使用了并发编程却仍旧不具有并发编程的可伸缩性。
Matt Welsh的SEDA是当代实现阶段化任务执行理念的一个朝着更为理性方向发展的实例。实际上,SEDA是个非常好的例子,它"具有非常恰当的服务器体系结构”,所以它的一些具体特性非常值得拿来说一说(特别是同我在上文中总结的不大相同的特性)。
1、SEDA的“批处理(batching)”倾向于强调在一个阶段中同时处理多个请求,而我的方法更倾向于强调同时在多个阶段中处理同一个的请求。
2、在我看来,SEDA的一个重大缺陷是它为每个阶段分配了一个单独的线程池,只是在“后台”根据负载情况对线程进行重新分配。这样一来,引起上下文切换的头号和第二号原因仍然会不断出现。
3、从学术研究项目的角度讲,用Java来实现SEDA也许可以说得过去。但从实际应用角度来讲,我认为这种选择可以说是很令人遗憾的。
内存分配
分配和释放内存是许多应用程序中最常见的操作之一。因此,人们为了让通用的内存分配器更加的高效而开研究出了许多巧妙的花招。然而,正是由于这些内存分配器的通用性,使得它们不可避免地会在许多场合下其效率远低于它们的其它替代方案,而且即使再巧妙也无法避免这种情况的发生。因此,关于如何彻底避免系统内存分配器,我有三个建议。
建议一是使用一个简单的预分配方案。我们都知道,静态内存分配在对程序的功能会施加人为限制的情况下最好不要谁好用,但是,预分配还有其它我们能够从中获益匪浅的很多种形式。通常使用预分配的原因来自于只调用一次系统内存分配器比调用多次好,即使在这个过程中会有部分内存被“浪费掉”了。所以,如果有可能可以断定,同时使用的数据不会多于N项,在程序启动之初就先进行内存预分配可能会是个正确的选择。即使无法做出这样的断定,为请求处理器在开始时就会需要进行分配的内存进行预分配可能要比随着需要一点一点来分配内存强;而且通过一次调用系统内存分配器就为许多个数据项而分配的内存还可能是连续的,这往往会极大的降低错误恢复代码的复杂度。在内存非常紧张的情况下,预分配就不是一个好的选择了,但是除了一些最极端的情况,其它情况下预分配一般都会是个绝对上算的选择。
建议二是采用后备列表(lookaside list)来对分配和释放的频率比较高的对象进行管理。其基本的想法是要将最近要释放的对象放入该列表而不是真正的释放它们,希望其后不久再需要它们的时候只需从后备列表中将它们重新取回来而不是从系统内存中为它们再次分配内存。使用后备列表还会带来一个额外的好处,就是在实现从后备列表中传进/传出复杂对象时,我们可以跳过对这些复杂对象的初始化/终止化(initialization/finalization)操作。
即使程序在空闲状态时也永不让真正释放所有对象,就会让后备列表无限制地增长下去,通常情况下这种做法是不可取的。因此,一般都很有必要设立一个周期性的“清扫者”任务来释放非活跃对象,但是如果因引入清扫者而增加了锁的复杂度以及出现锁争用的几率,那么这也同样是不可取的。这里有一个比较好的折中的办法,把后备列表分为两个独立锁定的“旧”列表和“新”列表。使用时首选从新列表中分配对象,然后是从旧列表中分配,万不得已时才从系统中分配对象;对象总是释放到新列表中。清扫者线程要按照下来步骤进行操作:
1、锁定这两个列表。
2、将旧列表的头指针保存起来。
3、通过列表的头指针赋值将(先前的)新列表转变为旧列表。
解锁。
4、在空闲时将在第二步保存起来的旧列表中的所有对象都释放掉。
5、在这种系统中,对象只有在至少一个完整的清扫周期且最多绝对不会超过两个清扫周期的时间内没有被使用到后,才会被真正的释放掉。更重要的是,清扫者线程在做的大部分工作时都不会和普通线程发生锁争用。从理论上讲,同样的方法也可以推广到多于两个处理阶段的系统中,但我还没有看出来这种推广有多大的实用价值。
使用后备列表有一个让人担心的问题是列表指针可能会增加对象的大小。从我的经验来看,我用后备列表来管理的绝大多数对象反正都已经包含了列表指针,所以这个问题没有什么太大的意义。但是,即使指针只是用于后备列表的,因为后备列表避免了多次调用系统内存分配器(而且还避免了对象初始化操作的多次执行),用由此而节省下来的系统开销来弥补列表指针所占用的那点额外的内存还是绰绰有余的。
建议三实际上同我们还尚未讨论的锁有关系,但无论如何我在这里要先说几句。通常在分配内存时,最大的开销化在了锁争用上,即使使用后备列表情况也是这样的。有个解决办法就是维护多个私有的后备列表,如此一来每个列表都绝不可能再发生锁争用的情况。例如,你可以为每个线程创建一个单独的后备列表。基于缓存热度(cache-warmth)方面的考虑,为每个处理器创建一个列表可能会更好,但这只有在非抢占式线程环境下才可行。为了创建内存分配开销极低的系统,如有必要,私有的后备列表甚至还可以同共享的后备列表结合起来使用。
锁的争用
众所周知,要设计出高效的锁定机制是极其困难的, 我将造成这个困难的原因称为斯库拉和卡律布狄斯(译者注:这两个名字放到一起在英语中的意思一般是让人进退两难、腹背受敌的意思),她俩是古希腊史诗《奥德赛》中的两个女妖。斯库拉代表非常简化和/或粗粒度的锁,她会把本可以或本应该并行进行的活动转化为必须按顺序执行的活动,因而会对性能和可伸缩性造成损失;卡律布狄斯代表超复杂或细粒度的锁,但她需要锁的地方太多再加上锁操作占用的时间同样也会造成性能损失。 靠近斯库拉的陷阱代表着发生死锁和活锁(deadlock and livelock)的情况;靠近卡律布狄斯的陷阱代表着竞态条件(race condition)。在这二者之间有一条狭窄的通道,它代表着即高效又正确的锁。。。但是这样的通道在哪里呢?因为锁一般会和程序逻辑紧密的联系在一起,所以在不深刻改变程序运行基础的情况下,想要设计出很好的锁定方案往往都是不太可能的。这就是人们为什么憎恨锁,并努力为他们采用不具伸缩性的单线程方案正名的原因。
几乎每一个锁定方案开头都会设计成“一个可以锁住所有东西的大锁”并且心存侥幸,希望这种设计性能不会糟到哪里去。这种侥幸心理往往不能得逞,当希望破灭后,大锁就会被分解成许多个相对较小的锁并继续心存侥幸,随后在重复一遍这个过程,大概在性能基本说的过去时整个设计过程才会结束。 通常每个迭代过程都会将程序的复杂度和锁操作的开销提高20-50%,但只能减少5-10%的锁争用。幸运的话,最终还是会在性能方面有适度的提高的,但性能没有提升却反而出现下降也并不罕见。设计者会因此而感到一头雾水,心里想:“我是按照所有的教科书教我的办法将锁的粒度调整到更小的,可为什么性能却更糟了呢?”
依我看,情况变得更糟了的原因在于,上文中所说的那个方法完全是被误导了。请把这个设计问题的“解空间”想象为一个山脉,山脉中的高地代表着优秀的设计方案,而山脉中的低处代表着糟糕的方案。上文中的问题就在于,开始时的那个“大锁”同山脉中的高峰间横亘着各种各样的山谷、山鞍、小山峰和绝路。这是个经典的爬山问题;从这样的一个起点开始,试图通过一小步一小步的方式爬到更高的山峰还不想走下坡路基本上是件不可能实现的事情。设计者所需要的是应该采用一种完全不同方式来爬向顶峰。
你要做的第一件事就是在你的头脑中要对你的程序的锁操作有一个示意图,该图具有两个轴:
●纵轴表示的是代码。如果你采用的是无分支阶段的阶段化架构,那很可能你已经有了一张类似大家在学习OSI模型中的网络协议栈时所使用的那种图。
●横轴表示的是数据。在每个阶段中,每个请求都应该分配到一个单独的数据集之中,该数据集包含着属于请求本身的资源。
这样你就会得到一个网格,网格中的每个单元格表示的是某特定处理阶段中的某特定数据集。其中最重要的是这个规则:两个请求间不应该发生争用,除非这两个请求处于同一个数据集并且处于同一个处理阶段。如果你能做到严格遵守这个规则,那么你就已经成功一半了。
上面的网格各项内容都弄明确之后,你就能够画出你的程序中所有类型的锁操作了,你的下一个目标是确保最终的画出来点在两个轴的方向上的分布要越均匀越好。很不幸,这部分工作同具体应用的相关性很大,你得象钻石切割师那样,根据你对程序要达到什么目的的了解,找出阶段和数据集间最自然的“切割线”。有时开头就能非常容易的找出来,有时就比较困难了,但貌似通过反复琢磨才能更容易的找到这些切割系。将代码分割到若个个阶段之中是程序设计中比较复杂的一件事,所以这块我也没有什么太多要说的,关于如何定义数据集我倒是有几个建议:
●如果请求有与之相关联的某种形式的批号、哈希或者事务ID,那除了将这个值除以数据集的总数之外,很少有其它更好的做法。
●有时,最好是基于哪个数据集具有最大可用资源而不是请求内存的属性将请求动态地分配给数据集。我们可以把数据集看作现代CPU中的多个整数单元;这些整数单元还知道点如何在系统中发出离散请求流。
●要确保每个阶段的数据集分配方案互不相同,这样一来,在一个阶段中会发生争用的请求就能够保证不会在另一个阶段中也发生争用了。这个建议通常会对你很有帮助作用。
如果你从纵横两个方向对你的“锁空间”进行了分割,并且的确所有的锁活动都最终的单元格中呈均匀分布,你就可以相对自信的说,你设计的锁定方案一定错不了。但这里还有一个步骤要走。你还记得我在本文中的前若个段中嘲笑过的那个“一小步一小步往前走”的方法吗?它仍然没有消失,但是(译者注:原文此处用的不是“但是”而是“因为”,这貌似同上下文有所违和。)现在你站在了一个好的起点之上,这同前几段中的那个方法不同。以比喻的方式来说,你可能已经站在了整个山脉中最高峰的一个山坡之上了,但很可能你还不是在峰顶之上。接下来就到时候去做下面这些事情了:收集争用统计数据从而找出你需要在哪些方面做出改善、以不同的方式来划分阶段和数据集并收集更多的统计数据直到你满意为止。在你完成这些任务之后,你就一定能够欣赏到山顶之上无限美好的景色了。
补充说明
正如我在文章开头时所说的一样,我讨论了服务器设计中四个最大的问题。但是,除此之外,任何特定的服务器都还需要解决一些其它的重要问题。归根结底,这些问题绝大多数都同你应该全面了解你所使用的平台/环境有关:
●你的存储子系统在处理数据规模比较大请求和数据规模比较比较小的请求时,其性能表现有何不同?在顺序访问和随机访问的情况下,其性能表现又有何不同?你的存储子系统在预读和后写方面做得怎么样?
●你所采用的网络协议效率怎么样?该协议是否具有能够提高其性能的配置参数或标志位?它是否有类似TCP_CORK、MSG_PUSH或者Nagle开关等待手段让你可以拿来避免出现超小消息吗?
●你的系统是否支持分散/集中IO(例如,readv/writev)?使用这种IO可以提高性能,还能够消除使用缓冲区链的痛苦。
●你所使用的内存页是多大的?缓存线有多大?是否有必要按照这些边界对齐?同其它操作相比,系统调用或上下文切换的开销有多大?
●你所使用的读写锁原语会不会出现互斥等待?会为谁而互斥等待呢? 你用的事件机制是否有“惊群”问题(译者注:wikipedia对这个惊群问题有解释)? 你所使用的休眠/唤醒机制是否有这种让人讨厌(但很常见)的行为:但X唤醒Y时,即便在X仍有事要做的情况下,上下文也会切换到Y?
毫不夸张地讲,沿着这个思路我还能想出更多的问题。我相信你也能。 在有些情况下,可能这些问题还不值得你真正花时间为它们做点什么,但通常它们至少还属于值得你去思考的问题。大部分问题的答案你从系统文档中是找不到的,如果你不知道这些答案,那么 去找吧!写个测试程序或者是小型基准测试程序来在实践中找出答案吧;不管怎样,编写这些代码本身就是一种很有用的技能。如果你写的代码要运行在多个平台之上,那这些问题中的大部分问题可能都需要你将功能抽象到各平台字节的代码库中,这样就能够根据平台支持的特性不同,实现在某个平台上获得单独的性能提高。
有个“知道所有答案”的理论同样适用于你自己的代码。弄清你的代码中比较重要的高层操作在哪里并在不同的情况下对它们执行所花的时间进行统计。这和传统的性能分析并不完全相同;这是在衡量设计元素,而不是真正的实现。底层优化一般是那些把设计搞砸了的人最后的救命稻草。
原文:http://mp.weixin.qq.com/s?__biz=MjM5NzA1MTcyMA==&mid=201256005&idx=3&sn=b5ec106c297c2b47916cad0fbfac169e&key=ee2bd3a4f7adcc4d63cd9533e6535b6b9436a8c430a694b8587fc5fff4318fc9fcd6fea91305fa294508f89b9ee576a3&ascene=1&uin=NTM2MzAzMjQw&devicetype=webwx&version=70000001&pass_ticket=B5reahDd9POpWs2Qgx6Ti0f9aywiGtIA2X%2F%2Fs3zCQbe7OzZb9%2FSwDm8DWdCQ27fK