你是否正打算优化hashCode()方法?是否想要绕开正则表达式?Lukas Eder介绍了很多简单方便的性能优化小贴士以及扩展程序性能的技巧。
最近“全网域(Web Scale)”一词被炒得火热,人们也正在通过扩展他们的应用程序架构来使他们的系统变得更加“全网域”。
但是究竟什么是全网域?或者说如何确保全网域?扩展的不同方面全网域被炒作的最多的是扩展负载(Scaling load),比如支持单个用户访问的系统也可以支持10 个、100个、甚至100万个用户访问。
在理想情况下,我们的系统应该保持尽可能的“无状态化(stateless)”。
即使必须存在状态,也可以在网络的不同处理终端上转化并进行传输。
当负载成为瓶颈时候,可能就不会出现延迟。
所以对于单个请求来说,耗费50到100毫秒也是可以接受的。
这就是所谓的横向扩展(Scaling out)。
扩展在全网域优化中的表现则完全不同,比如确保成功处理一条数据的算法也可成功处理10条、100条甚至100万条数据。
无论这种度量类型是是否可行,事件复杂度(大O符号)是最佳描述。
延迟是性能扩展杀手。
你会想尽办法将所有的运算处理在同一台机器上进行。
这就是所谓的纵向扩展(Scaling up)。
如果天上能掉馅饼的话(当然这是不可能的),我们或许能把横向扩展和纵向扩展组合起来。
但是,今天我们只打算介绍下面几条提升效率的简单方法。
大O符号Java 7的ForkJoinPool和Java8 的并行数据流(parallel Stream)都对并行处理有所帮助。
当在多核处理器上部署Java程序时表现尤为明显,因所有的处理器都可以访问相同的内存。
所以,这种并行处理较之在跨网络的不同机器上进行扩展,根本的好处是几乎可以完全消除延迟。
但不要被并行处理的效果所迷惑!请谨记下面两点:∙并行处理会吃光处理器资源。
并行处理为批处理带来了极大的好处,但同时也是非同步服务器(如HTTP)的噩梦。
有很多原因可以解释,为什么在过去的几十年中我们一直在使用单线程的Servlet模型。
并行处理仅在纵向扩展时才能带来实际的好处。
∙并行处理对算法复杂度没有影响。
如果你的算法的时间复杂度为O(nlogn),让算法在c 个处理器上运行,事件复杂度仍然为O(nlogn/c),因为c 只是算法中的一个无关紧要的常量。
你节省的仅仅是时钟时间(wall-clock time),实际的算法复杂度并没有降低。
降低算法复杂度毫无疑问是改善性能最行之有效的办法。
比如对于一个HashMap 实例的lookup() 方法来说,事件复杂度O(1) 或者空间复杂度O(1) 是最快的。
但这种情况往往是不可能的,更别提轻易地实现。
如果你不能降低算法的复杂度,也可以通过找到算法中的关键点并加以改善的方法,来起到改善性能的作用。
假设我们有下面这样的算法示意图:该算法的整体时间复杂度为O(N3),如果按照单独访问顺序计算也可得出复杂度为O(N x O x P)。
但是不管怎样,在我们分析这段代码时会发现一些奇怪的场景:∙在开发环境中,通过测试数据可以看到:左分支(N->M->Heavy operation)的时间复杂度M 的值要大于右边的O 和P,所以在我们的分析器中仅仅看到了左分支。
∙在生产环境中,你的维护团队可能会通过AppDynamics、DynaTrace 或其它小工具发现,真正导致问题的罪魁祸首是右分支(N -> O -> P -> Easy operation or also N.O.P.E.)。
在没有生产数据参照的情况下,我们可能会轻易的得出要优化“高开销操作”的结论。
但我们做出的优化对交付的产品没有起到任何效果。
优化的金科玉律不外乎以下内容:∙良好的设计将会使优化变得更加容易。
∙过早的优化并不能解决多有的性能问题,但是不良的设计将会导致优化难度的增加。
理论就先谈到这里。
假设我们已经发现了问题出现在了右分支上,很有可能是因产品中的简单处理因耗费了大量的时间而失去响应(假设N、O和P 的值非常大),请注意文章中提及的左分支的时间复杂度为O(N3)。
这里所做出的努力并不能扩展,但可以为用户节省时间,将困难的性能改善推迟到后面再进行。
这里有10条改善Java性能的小建议:1、使用StringBuilderStingBuilder 应该是在我们的Java代码中默认使用的,应该避免使用+ 操作符。
或许你会对StringBuilder 的语法糖(syntax sugar)持有不同意见,比如:1 String x = "a" + args.length + "b";将会被编译为:1 2 3 4 5 6 7 8 91011 0 new ng.StringBuilder [16]3 dup4 ldc <String "a"> [18]6 invokespecial ng.StringBuilder(ng.String) [20]9 aload_0 [args]10 arraylength11 invokevirtual ng.StringBuilder.append(int) : ng.StringBuilder 14 ldc <String "b"> [27]16 invokevirtual ng.StringBuilder.append(ng.String) :ng.StringBuilder [29]19 invokevirtual ng.StringBuilder.toString() : ng.String [32] 22 astore_1 [x]但究竟发生了什么?接下来是否需要用下面的部分来对String 进行改善呢?1 2 3 4 String x = "a" + args.length + "b";if (args.length == 1)x = x + args[0];现在使用到了第二个StringBuilder,而且这个StringBuilder 不会消耗堆中额外的内存,但却给GC 带来了压力。
1 2 3 4 5 6 StringBuilder x = new StringBuilder("a"); x.append(args.length);x.append("b");if (args.length == 1);x.append(args[0]);小结在上面的样例中,如果你是依靠Java编译器来隐式生成实例的话,那么编译的效果几乎和是否使用了StringBuilder 实例毫无关系。
请记住:在N.O.P.E 分支中,每次CPU的循环的时间到白白的耗费在GC或者为StringBuilder 分配默认空间上了,我们是在浪费N x O x P 时间。
一般来说,使用StringBuilder 的效果要优于使用+ 操作符。
如果可能的话请在需要跨多个方法传递引用的情况下选择StringBuilder,因为String 要消耗额外的资源。
JOOQ在生成复杂的SQL语句便使用了这样的方式。
在整个抽象语法树(AST Abstract Syntax Tree)SQL传递过程中仅使用了一个StringBuilder 。
更加悲剧的是,如果你仍在使用StringBuffer的话,那么用StringBuilder 代替 StringBuffer吧,毕竟需要同步字符串的情况真的不多。
2、避免使用正则表达式正则表达式给人的印象是快捷简便。
但是在N.O.P.E 分支中使用正则表达式将是最糟糕的决定。
如果万不得已非要在计算密集型代码中使用正则表达式的话,至少要将 Pattern缓存下来,避免反复编译Pattern。
1 static final Pattern HEAVY_REGEX =2 pile("(((X)*Y)*Z)*");如果仅使用到了如下这样简单的正则表达式的话:1 String[] parts = ipAddress.split("\\.");这是最好还是用普通的char[] 数组或者是基于索引的操作。
比如下面这段可读性比较差的代码其实起到了相同的作用。
1 2 3 4 5 6 7 8 9101112 int length = ipAddress.length();int offset = 0;int part = 0;for (int i = 0; i < length; i++) {if (i == length - 1 ||ipAddress.charAt(i + 1) == '.') {parts[part] =ipAddress.substring(offset, i + 1);part++;offset = i + 2;}}上面的代码同时表明了过早的优化是没有意义的。
虽然与split() 方法相比较,这段代码的可维护性比较差。
挑战:聪明的小伙伴能想出更快的算法吗?小结正则表达式是十分有用,但是在使用时也要付出代价。
尤其是在N.O.P.E 分支深处时,要不惜一切代码避免使用正则表达式。
还要小心各种使用到正则表达式的JDK字符串方法,比如String.replaceAll()或String.split()。
可以选择用比较流行的开发库,比如Apache Commons Lang来进行字符串操作。
3、不要使用iterator()方法这条建议不适用于一般的场合,仅适用于在 N.O.P.E 分支深处的场景。
尽管如此也应该有所了解。
Java 5格式的循环写法非常的方便,以至于我们可以忘记内部的循环方法,比如: 1 2for (String value : strings) {// Do something useful here2345 int cursor; int lastRet = -1; int expectedModCount = modCount; // ...也可以用下面等价的循环方式来替代上面的 for 循环,仅仅是在栈上“浪费”了区区一个整形,相当划算。
12345 int size = strings.size(); for (int i = 0; i < size; i++) { String value : strings.get(i); // Do something useful here } 如果循环中字符串的值是不怎么变化,也可用数组来实现循环。
1 2 3 for (String value : stringArray) {// Do something useful here}小结无论是从易读写的角度来说,还是从API 设计的角度来说迭代器、Iterable 接口和 foreach 循环都是非常好用的。