性能优化的原则

我最近的一条tweet被retweet,点赞和评论次数远超我平常发的东西,所以我觉得应该扩展一下我自己对这个问题的想法。

性能优化的原则:
1)从第一天开始就针对性能进行设计
2)经常调研分析
3)警惕性能的倒退
4)理解数据
5)理解硬件
6)帮助编译器
7)验证自己的假设
8)性能是每个人的事情

Programming Wisdom引用了一条tweet,认为几乎任何时间都不是考虑性能优化的好时机。即使专家也要把这件事推迟。其实这个论调比你常听到的“过早优化是万恶之源”还要差劲。过早的优化这句话至少指出了一些的问题,即程序员为了速度上手把一些代码弄的模糊不清,甚至不管这段代码是否有可优化的性能问题,也不会去验证新的代码是否真的更快了,而在优化的过程中又引入了新的bug或者降低了稳定性。是的,这是实际的问题,也是不好的实践方式。这才是Knuth这段话的实际的意思。但是这段话接着的内容也同样重要:“但我们仍然不能放过那关键的3%的机会”。所以他所说的是你应该先分析代码,发现瓶颈的位置,然后去优化这些代码。这是我同意的好的建议。

我想在Knuth这段名言的基础上更进一步。如果你工作的环境中有着很好的性能优化的文化,实际上你可以打开性能分析器,找到你最慢的3%的函数(或者最慢的3%的着色器),优化它们,得到一个拿得出手的产品。如果针对性能的工作在多数的工作环节中被忽略了,当你最终启动性能分析器时,你会发现很多系统性的性能问题。修复最慢的3%的函数,只会带来很小的可以观察到的效果,或者甚至完全无效。你会发现你的产品,如果没有大规模的重新设计,更多的项目延误,成本增加或者砍掉功能,怎么也到不了能够拿得出手的状态。只要一点点性能的提升就够了,如果你怀着这样的希望,你绝对不想让团队的花6个月时间去拯救一个在基础上就有有问题的产品。

你需要的是一个关注性能的文化。能够理解性能是产品的一个核心特性。差劲的性能意味着差劲的用户体验,或者从游戏的角度来说,几乎就是不能玩也不能发布的。当你设计新的系统时,需要从一开始就考虑性能。是的,你可以在攒原型产品或者概念实现的时候,不去过度考虑细节上的优化。你可以跑一阵这个产品,了解一下运营的情况。没问题,在这个节点上都没有问题。但是这时要考虑一下,当产品正式上线时,系统需要能承受的负载是什么样子的。系统可以在100个物品时是否正常?1000呢?1百万呢?系统会不会成长到1百万个物品。没有考虑过能否伸展到它最终所需要的规模时,不要把一个原型集成到生产系统中。这样的概念并非你要在一开始就把系统优化至最后一条SIMD指令或者最后一个字节的内存。但是你需要准备如果在这样的规模上运行时的方案。如果代码只需要处理不多的对象,不要过度的痴迷于性能。但是会有几百个对象吗?看看你是简单的算出来。也许你需要把对象分组。会有几千个对象吗?也许你需要保证这些对象被分批处理,要考虑内存的消耗,使用的模式和带宽,可能需要把活跃的和不活跃的对象区分开,或者采用层次分治的方法。几万个对象?也需要考虑一下线程了。几百万个对象?开始数一下访问的周期吧,已经不能再让缓存miss了。

这是我上面的列表中的第1条,也有第8条,我猜还要算上第4条和第5条。是的,这个列表并没有排序。我的tweet并没有特别用心的编排,只是被下面糟糕的建议刺激的。现在,假设你的工作更多是和已经存在的代码打交道,而不是写新的代码,你每天的工作可能是更多的花时间在解决bug,维护和修复代码而非优化。这里的想法并非性能的优先级应该高过其他的问题,而是说性能应该是和其他问题具有同等的价值。你不会建议别人推迟一个bug的修复到产品的最后阶段,那么你也不应该把性能的工作推迟到最后。就像你不能让团队中的一个工程师作为“the bug guy”来修复全部的问题而且其他人都开发新特性,所以让一个团队中的一个人成为“the performance guy”去修复全部的问题也是不合理的。

性能是每一个人的责任,性能需要一直伴随着整个过程。所以这就引入了第3条,你需要非常重视性能的倒退。如果性能意外的突然下降,那么调查和修复这个问题需要被排在最高的优先级。为了抓获性能的倒退,你需要一个系统来监测它,比如可以画出每日的性能图表的自动化测试。团队中的工程师需要足够频繁的分析来获取对于自己的游戏、应用或者自己的特殊系统的性能特点的足够的整体认知,所以无论什么东西看起来和平常不一样,那么要么是一些东西发生了故障,要么就是一个团队成员需要因为他突出的优化工作需要受到奖励了。

一般而言,你给性能优化工作分配的优先级会因为你开发的对象,性能的对系统的关键程度以及你当前的状态而不断变化。并非任何的软件都需要大量的性能方面的工作。我的tweet中的内容应该放在一个组织中的团队来理解,而我的观点也主要来自游戏开发的渲染工程师。在我的工作中,性能至关重要。我需要解决每秒钟60次对上万个对象的渲染,在毫秒级完成上百万像素点着色。也许你编写跑在自己服务器上的应用,你可以考虑直接扩容硬件。但是这在游戏开发中是行不通的,而且也许在很多产业中也是不行的。软件不是为我们自己开发的,硬件也不用我们自己的。我可以给内部工具升级硬件来优化服务器,比如构建服务器,但是消费者购买的产品必须跑在他们自己的机器上。如果我们针对PS4开发的产品不能流畅的跑在PS4上,我们必须等到这个达到后才能发布。

这里并不是说我们总是需要性能优化的工作。当我编写个人的代码时,我并不需要花很多时间来调优性能。至少不会超过需要,或者我应该说,不会超过我自己的意愿。可以只是直接遍历就好。如果我只想为一个场景生成一个lightmap,我可以写一些简单的代码,把光线的负载都扔给场景,直到全部的阴影都光滑起来。如果这样的任务只需要跑一次,跑30分钟也没有问题。所以除非我可以把代码优化到远远低于30分钟,更有效率的方法应该是就把这个没有优化过的代码放在那里。在实际中的优化中,说实话,我一般都需要跑几十次才能得到满意的结果。 

而且,如果你在和一个还存在很多bug的系统打交道,我同意这并非是一个启动优化的实际。但是不要仅仅因为代码中存在bug就推迟性能优化工作。在角色动画还有问题时优化遮挡剪裁额并没有什么问题,不过你当然需要首先保证剪裁的逻辑是正确的并且你要保证在开始优化前不会错误的剪裁掉任何对象。

所以,规则的第1,2,3条和第8条,主要关注的是性能优化的文化。我应该说这些点是最重要的。其余的条目,主要是在你已经开始坐下优化时,一些针对实践的建议。对于第4条和第5条,我没有什么要补充的,但对于第6条,帮助编译器,是的,编译器可以非常聪明,并且可以使用非常好的方法来加速你的代码。但是编译器经常被限制在一些路径中,并不会按照你期待的来优化代码。所谓的零成本抽象,当你堆放了很多层时,可能会变成非常高成本的抽象。或者当你在Debug模式运行时,一个编译器不仅仅会收到语义的限制(有时候语义会以令人惊奇方式约束编译器),也在一些情况下,编译器也会尝试一些优化,在整个过程中也会获得相当的成功,但是若干个迭代之后却不会收敛,然后生成的代码并非你说期待的inline的代码。验证你的假设是关键,看一下实际生成的汇编代码,是否编译器生成了你期待的代码?零成本抽象实际上是否真的没有生成任何指令?是否inline了玩去不需要inline的代码?

这里还可以说很多有关于性能优化的实践的内容,但是我最想说的是,性能非常重要,把性能优化推至开发的最后环节会导致灾难性的后果。

英文原文地址:http://www.humus.name/index.php?page=News&ID=383%20Rules%20of%20optimization


猜你喜欢

转载自blog.csdn.net/gstianfu/article/details/80931820