14 KiB
17 | 为什么需要经济的代码?
如果你在线购买过春运的火车票,经历过购票网站的瘫痪,你应该深有体会,网站瘫痪是一件让人多么绝望的事情。
根据有关报道,2014年1月9日,火车票售票网站点击量高达144亿次,相当于每个中国人点击了10次,平均每秒点击了16,000次,峰值的点击量可能远远超出16,000次。这么强悍的访问量,导致了火车售票网站多次瘫痪。这是一个典型的性能错配导致的重大网络事故,处理这么大的点击量需要特殊的程序设计和架构安排。
有句俗话说:“又要马儿跑,又要马儿不吃草。”马该怎么活呀?活不了呀!要想让马跑得快,既要有好马,也要有好料。
如果可以把软件比作一匹马的话,让这匹马出生时有一个优良的基因,平日里精心地伺候,是让它跑得快的先决条件。
前一段时间,我们讨论了如何让代码“写得又快又好、读得又快又好”的话题。接下来的这段时间,我们来聊聊怎么让代码“跑得又快又好”。跑得又快又好,一般也意味着更少的运营费用。该怎么让我们写的代码有一个跑得好的基因呢?
需不需要“跑得快”的代码?
很多项目是面向订单的,代码的功能是需要优先考虑的任务。这并没有错误。如果不能兼顾性能,这个债将来还起来会很痛苦,成本很高。而且,很多情况下,它是躲不开、赖不掉的。
怎么理解代码的性能?
为了理解这个问题,我们需要对代码的性能有一个共同的认识。代码的性能并不是可以多块地进行加减乘除,而是如何管理内存、磁盘、网络、内核等计算机资源。代码的性能与编码语言关系不大,就算是JavaScript编写的应用程序,也可以很快,C语言编写的程序也可能很慢。
事实上,代码的性能和算法密切相关,但是更重要的是,我们必须从架构层面来考虑性能,选择适当的技术架构和合适的算法。很多纸面上看起来优美的算法,实际上很糟糕。也有很多算法看起来不咋样,但实际上很高效。为了管理代码的性能,在一定程度上,我们需要很好地了解计算机的硬件、操作系统以及依赖库的基本运行原理和工作方式。一个好的架构师,一定会认真考虑、反复权衡性能要求。
需不需要学习性能?
一个程序员,可以从多个方面做出贡献。有人熟悉业务逻辑,有人熟悉类库接口,有人能够设计出色的用户界面。这都非常好,但是如果考察编程能力,有两件事情我们需要特别关注。
第一件事情是,我们的代码是不是正确?事实上,代码正确这个门槛特别低。如果代码出现了大范围的错误,说明编程还没有入门。
第二件事情是,我们的代码运行起来有没有效率,运营成本低不低?这也是我们判断代码是否经济的一个标准。编写经济的代码的门槛稍微高一些,它需要更多的知识和经验,但它也是能让我们脱颖而出的一个基本功。门槛越高,跨越门槛的价值就越大。我们要是一直不愿意跨越这个高门槛,面临的竞争压力就会越来越大。
这个价值到底有多大呢?就我熟悉的领域来说,如果你可以把Java垃圾管理器的效率提高50%,或者把列表的查询速度提高50%,更或者,你能用三五台服务器解决掉春运火车票售票网站崩溃的问题,那么找到一份年薪百万的工作是不难的。
当然上面的一些问题实现起来非常困难,比如提高Java垃圾管理器的效率。但是,需要我们解决的性能问题,很多时候,都不是技术问题,而是意识和见识的问题。成熟的解决方案就在那儿,容易理解,也容易操作。只是,我们没有想到,没有看到,也没有用到这些解决方案。我们越不重视性能,这些知识离我们就越远。
一个好的程序员,他编写的代码一定兼顾正确和效率的。事实上,只有兼顾正确和效率,编程才有挑战性,实现起来才有成就感。如果丢弃其中一个指标,那么大多数任务都是小菜一碟。
有过面试经验的小伙伴,你们有没有注意到,正确和有效地编码是面试官考察的两个重点?招聘广告可不会提到,程序员要能够编写正确的代码和有效的代码。但是一些大的企业,会考察算法,其中一条重要的评判标准就是算法够不够快。他们可能声称算法考察的是一个人的基本功,是他的聪明程度。但是如果算法设计不够快,主考官就会认为我们基本功不够扎实、不够聪明。 你看,算法快慢大多只是见识问题,但很多时候,会被迫和智商联系起来。这样做既无理,也无聊,但是我们也没有办法逃避开来,主考官可能也没有更好的办法筛选出更好的人才。
需不需要考虑代码性能?
具体到开发任务,对于软件的性能,有很多误解。这些误解,一部分来自我们每个人都难以避免的认知的局限性,一部分来自不合理的假设。
比如说,有一种常见的观点是,我们只有一万个用户,不要去操百万用户的心。这种简单粗暴的思考方式很麻烦!你要是相信这样的简单论断,肯定会懵懂得一塌糊涂。百万用户的心是什么心?你根本没有进一步思考的余地。你唯一能够理解的,大概就是性能这东西,一边儿玩去吧。
一开始,我们就希望大家能从经济的角度、从投入产出的角度、从软件的整个生命周期的角度来考虑代码。我们要尽量避免这种不分青红皂白,一刀切下去的简单方式。这种简单粗暴的方式可能会帮我们节省几秒钟的时间,我们思考的快系统喜欢这样,这是本性。但是,我们真的没必要在乎这几秒钟、几分钟,甚至是几小时,特别是在关乎软件架构和软件质量的问题上。该调用我们思考的慢系统的时候,就拿出来用一用。
我们可以问自己一些简单的问题。比如说,一万个用户会同时访问吗?如果一秒钟你需要处理一万个用户的请求,这就需要有百万用户、千万用户,甚至亿万用户的架构设计。
再比如说,会有一万个用户同时访问吗?也许系统没有一万个真实用户,但是可能会有一万个请求同时发起,这就是网络安全需要防范的网络攻击。系统保护的东西越重要,提供的服务越重要,就越要防范网络攻击。而防范网络攻击,只靠防火墙等边界防卫措施,是远远不够的,代码的质量才是网络安全防护的根本。
你看,哪怕我们没有一万个用户,我们都要操一万个用户的心;当我们操一万个用户的心的时候,我们可能还要操百万用户的心。
你有没有意识到,你操心的程度和用户量的关系不是那么紧密?你真正需要关心的,是你的代码有多重要? 代码带来的绝对价值越大,消耗的绝对成本越高,它的性能就越重要。
当然,也不是所有的软件都值得我们去思考性能问题。有人统计过,大概90%以上的软件,都没有什么实际用处,也就是说,运营价值非常小。比如我们的毕业论文代码,比如入门教科书的示例代码,比如我们为公司晚会写的、用完就扔的抽奖程序。这是对的,大多数代码的性能优化是无用的,因为它们并没有多大的实际运营价值。
但是,如果我们要去开发具有商业价值的软件,就要认真思考代码的性能能够给公司带来的价值,以及需要支付的成本。
经验告诉我们,越早考虑性能问题,我们需要支付的成本就越小,带来的价值就越大。甚至是,和不考虑性能的方案相比,考虑性能的成本可能还要更小。
你可能会吃惊,难道优化代码性能是没有成本的吗? 当然有。这个成本通常就是我们拓展视野和经验积累所需要支付的学费。这些学费,当然也变成了我们自身市场价值的一部分。
有时候,有人说:“我们只有一万个用户,不要去操百万用户的心。” 其实,潜台词是说,我们还没有技术能力去操一百万个用户的心,也没有时间或者预算去支付这样的学费。这其实对我们是有利的。一旦我们有了这样的见识和能力,我们就可以发挥市场的价值。这是一个可以赚回学费的机会,也会让我们变得越来越有价值。
什么时候开始考虑性能问题?
为了进度,很多人的选择是不考虑什么性能问题,能跑就行,先跑起来再说;先把代码摞起来,再考虑性能优化;先把业务推出去,再考虑跑得快不快的问题。可是,如果真的不考虑性能,一旦出了问题,系统崩溃,你的老板不会只骂他自己,除非他是一个优秀的领导。
硬件扩展能解决性能问题吗?
有一个想法很值得讨论。很多人认为,如果碰到性能问题,我们就增加更多的机器去解决,使用更多的内存,更多的内核,更快的CPU。网站频繁崩溃,为什么就不能多买点机器呢?!
但遗憾的是,扩展硬件并不是总能够线性地提高系统的性能。出现性能问题,投入更多的设备,只是提高软件性能的一个特殊方法。而且,这不是一个廉价的方法。过去的经验告诉我们,提高一倍的性能,硬件投入成本高达四五倍;如果需要提高四五倍的性能,可能投入二三十倍的硬件也达不到预期的效果。硬件和性能的非线性关系,反而让代码的性能优化更有价值。
性能问题能滞后处理吗?
越来越多的团队开始使用敏捷开发模式,要求拥抱变化,快速迭代。很多人把这个作为一个借口:我们下一次迭代的时候,再讨论性能问题。他们忘了敏捷开发最重要的一个原则,就是高质量地工作。没有高质量的工作作为基础,敏捷开发模式就会越走越艰难,越走越不敏捷,越走成本越高。而性能问题,是最重要的质量指标之一。
性能问题,有很多是架构性问题。一旦架构性问题出现,往往意味着代码要推倒重来,这可不是我们可以接受的快速迭代。当然,也有很多性能问题,是技术性细节,是变化性的问题。对于这些问题,使用快速迭代就是一个经济的方式。
很多年以来,我们有一个坏的研发习惯,就是性能问题滞后处理,通过质量保证(QA)环节来检测性能问题,然后返回来优化性能。这是一个效率低、耗费大的流程。
当应用程序进入质量保证环节的时候,为时已晚。在前面的设计和开发阶段中,我们投入了大量时间和精力。业务也要求我们尽快把应用程序推向市场。如果等到最后一分钟,才能找到一个严重的性能问题,推迟产品的上市时间,错失市场良机,那么这个性能问题解决的成本是数量级的。没有一个企业喜欢事情需要做两遍才能做到正确的团队,所以我们需要在第一时间做到正确。
要有性能工程的思维
采用性能工程思维,才能确保快速交付应用程序,而不用担心因为性能耽误进度。性能工程思维通过流程“左移”,把性能问题从一个一次性的测试行为,变成一个贯穿软件开发周期的持续性行为;从被动地接受问题审查,变成主动地管理质量。也就是说,在软件研发的每一步,每一个参与人员,都要考虑性能问题。整个过程要有计划,有组织,能测量,可控制。
采用性能工程思维,架构师知道他们设计的架构支持哪些性能的要求;开发工程师清楚应该使用的基本技术,而不是选择性地忽略掉性能问题;项目管理人员能够在开发软件过程中跟踪性能状态;性能测试专家有时间进行负载和压力测试,而不会遇到重大意外。实现性能要求的风险在流程早期得到确认和解决,这样就能节省时间和金钱,减轻在预算范围内按时交付的压力。
现在很多公司的研发,完美地匹配了敏捷开发和性能工程这两种模式。降低研发成本的同时,也促进了员工的成长,减轻了程序员的压力。
小结
最后,我们总结一下。编写有效率的代码是我们的一项基本技能。我们千万不要忽视代码的性能要求。越早考虑性能问题,需要支付的成本就越小,带来的价值就越大,不要等到出现性能问题时,才去临时抱佛脚。另外,性能问题,大部分都是意识问题和见识问题。想得多了,见得多了,用得多了,技术就只是个选择的问题,不一定会增加我们的编码难度和成本。
接下来的这一模块,我们会聚焦在解决性能问题的一些基本思路和最佳实践上,比如架构设计问题、内存管理问题、接口设计问题和开发效率问题等等。
最后问你个问题吧,你有因为性能问题承担过巨大的压力吗?这个性能问题是怎么来的?最后怎么解决的?欢迎你在留言区分享你的想法。
一起来动手
下面的这段代码,我们前面使用了很多次,主要是为了学习编码规范。其实,它也有性能问题。这一次,我们来试着优化它的性能。
我先要说明的是,如果你之前没有接触过类似的问题,那么它是有点难度的。如果你已经接触过类似的问题,这个问题就是小菜一碟。这就是一个见了多少、经验也就有多少的问题。
欢迎你把优化的代码公布在讨论区,我们一起来看看性能优化后的代码可以是什么样的?
import java.util.HashMap;
import java.util.Map;
class Solution {
/**
* Given an array of integers, return indices of the two numbers
* such that they add up to a specific target.
*/
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
}