找回密码
 立即注册

QQ登录

只需一步,快速开始

断天涯大虾
社区贡献组   /  发表于:2017-3-1 17:43  /   查看:4596  /  回复:0
本帖最后由 断天涯大虾 于 2017-3-10 09:24 编辑

以下是Java中10个最简单的性能优化建议:

1. 使用 StringBuilder
这个应该成为几乎所有的Java代码里你默认要使用的东西,应该要尝试避免使用+操作符。当然你可能会争辩说, StringBuilder只是一个语法糖而已比如:
  1. <font face="微软雅黑">String x = "a" + args.length + "b";</font>
复制代码
… 这段代码会被编译为:
  1. new java.lang.StringBuilder [16]
  2. dup
  3. ldc <String "a"> [18]
  4. invokespecial java.lang.StringBuilder(java.lang.String) [20]
  5. aload_0 [args]
  6. arraylength
  7. invokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [23]
  8. ldc <String "b"> [27]
  9. invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [29]
  10. invokevirtual java.lang.StringBuilder.toString() : java.lang.String [32]
  11. astore_1 [x]
复制代码
但是如果稍后你需要使用可选部分来修正的字符串,那会发生什么?
  1. <font face="微软雅黑">String x = "a" + args.length + "b";

  2. if (args.length == 1)
  3.     x = x + args[0];</font>
复制代码
现在你将会有第二个StringBuilder,这样会白白的浪费你的堆内存,给你的GC增加压力。相反的,你可以这样写:
  1. <font face="微软雅黑">StringBuilder x = new StringBuilder("a");
  2. x.append(args.length);
  3. x.append("b");

  4. if (args.length == 1);
  5.     x.append(args[0]);</font>
复制代码

小结
在上面的例子中,如果你使用了显式的StringBuilder实例或者依赖Java编译器创建隐式的实例,那就可能是完全不相关的。但是请记住,我们是在NOPE分支

如果我们在每一个CPU周期里,做些如GC或者为StringBuilder分配默认空间这种愚蠢的事情的话,那我们其实是浪费了N x O x P 倍的时间。

一般来说,总是使用StringBuilder而不是使用+操作符。而且如果你的字符串过于复杂,你还可以让StringBuilder的引用跨多个方法。

当你使用jOOQ生成一个复杂的SQL语句时,它就是这么做的,也就是说只有一个StringBuilder对象来“遍历”你的整个SQL的 AST(抽象语法树Abstract Syntax Tree)。

而且,如果你仍然还在使用StringBuffer的引用,请务必将它们替换为StringBuilder,(因为)你几乎用不着在字符串上进行同步。

-----------------------------------------------------------------------------------------------------------------------------

2. 避免使用正则表达式
正则表达式相对来说是廉价且方便易用的。但是如果你在N.O.P.E.分支上 它们大概就是最糟糕的事情了。

如果你在计算密集型的代码段里确实必须要使用正则表达式,那么至少要将Pattern(模式)的引用进行缓存而不是每次都去编译它:
  1. <font face="微软雅黑">static final Pattern HEAVY_REGEX =
  2.     Pattern.compile("(((X)*Y)*Z)*");</font>
复制代码
但是如果你的正则表达式看起来像这样愚蠢的话:
  1. <font face="微软雅黑">String[] parts = ipAddress.split("\\.");</font>
复制代码
… 那么你真的最好去使用普通的char[] 或者基于索引的操作。 例如, 以下这个完全读不懂的循环代码做了相同的事情:
  1. <font face="微软雅黑">int length = ipAddress.length();
  2. int offset = 0;
  3. int part = 0;
  4. for (int i = 0; i < length; i++) {
  5.     if (i == length - 1 ||
  6.             ipAddress.charAt(i + 1) == '.') {
  7.         parts[part] =
  8.             ipAddress.substring(offset, i + 1);
  9.         part++;
  10.         offset = i + 2;
  11.     }
  12. }</font>
复制代码
…这段代码同时也表明了为什么你不要 过早的进行优化。相比于split()的版本,这段代码比较难于维护。
挑战:聪明的读者可能会发现有更快的算法。

小结
正则表达式是很有用,但是使用它们是要付出代价的。如果你在一个N.O.P.E. 分支深处时你就应该不惜一切代价的避免使用正则表达式。(因此)要小心那些使用正则表达式的JDK字符串方法, 比如 String.replaceAll(),或者String.split()

相反的,应该使用流行的类库像Apache Commons Lang,来进行字符串的操作。

-----------------------------------------------------------------------------------------------------------------------------

3. 不要使用iterator()
目前这条建议并不是适合一些通用的场合,而是只是用于N.O.P.E.分支的场景 。 尽管如此,你也应该考虑一下它。

编写Java-5风格的foreach循环是很方便的。你可以把内部循环完全忘掉,然后这样写:
  1. <font face="微软雅黑">for (String value : strings) {
  2.     // Do something useful here
  3. }</font>
复制代码
然而,每当代码运行到这个循环时,如果strings对象是一个Iterable,那么就会创建一个新的Iterator实例。

如果你在使用ArrayList,那么这将会在你的堆内存里创建一个对象,并分配3个int类型的空间:
  1. <font face="微软雅黑">private class Itr implements Iterator<E> {
  2.     int cursor;
  3.     int lastRet = -1;
  4.     int expectedModCount = modCount;
  5.     // ...</font>
复制代码
相反的,你可以像下面这样书写代码,一样效果的循环,而只是“浪费”了一个单独的栈里的整型值,相当的廉价:
  1. <font face="微软雅黑">int size = strings.size();
  2. for (int i = 0; i < size; i++) {
  3.     String value : strings.get(i);
  4.     // Do something useful here
  5. }</font>
复制代码
… 或者,如果你的列表不会变化的话,你甚至还可以在一个数组版本上面进行操作:
  1. <font face="微软雅黑">for (String value : stringArray) {
  2.     // Do something useful here
  3. }</font>
复制代码

小结
Iterators、Iterable和foreach循环从易写程度和可读性方面来说都是相当有用的,而且在API设计方面来看也是如此。然而,它们在每一次单独迭代时都会在堆内存里产生一个新的小实例。如果你多次运行这个迭代,你会想要尽量避免创建这个无用的实例,而且使用基于索引的迭代来代替。

讨论
对于上面的某些部分有一些比较有趣的分歧(特别是将Iterator的使用替换为通过索引进行访问),具体讨论内容请查阅Reddit

-----------------------------------------------------------------------------------------------------------------------------

4. 不要调用那些(特殊的)方法
一些方法简单但开销不小。在N.O.P.E.分支示例中我们没有在叶节点上使用这样的方法,但你可能使用到了。我们假设 JDBC 驱动程序需要耗费大量资源来计算 ResultSet.wasNull() 的值。你可能会用下列代码开发 SQL 框架:
  1. if ( type == Integer . class ) {
  2.     result = (T) wasNull(rs,
  3.         Integer .valueOf(rs.getInt( index )));
  4. }

  5. // And then ...
  6. static final <T> T wasNull(ResultSet rs, T value )
  7. throws SQLException {
  8.     return rs.wasNull() ? null : value ;
  9. }
复制代码
此处逻辑每次都会在你从结果集中获得一个 int 之后立即调用 ResultSet.wasNull()。但getInt() 的约定是:

返回类型:变量值;如果SQL查询结果为NULL,则返回0。

所以一个简单有效的改善方法如下:
  1. <font face="微软雅黑">static final <T extends Number> T wasNull (
  2.     ResultSet rs, T value
  3. )
  4. throws SQLException {
  5.      return ( value == null ||
  6.            ( value .intValue() == 0 && rs.wasNull()))
  7.         ? null : value ;
  8. }</font>
复制代码
这是轻而易举的事情。

小结
将方法调用缓存起来替代在叶子节点的高开销方法,或者在方法约定允许的情况下避免调用高开销方法。

-----------------------------------------------------------------------------------------------------------------------------

5、使用原始类型和栈
上面介绍了来自 jOOQ的例子中使用了大量的泛型,导致的结果是使用了 byte、 short、 int 和 long 的包装类。但至少泛型在Java 10或者Valhalla项目中被专门化之前,不应该成为代码的限制。因为可以通过下面的方法来进行替换:
  1. <font face="微软雅黑">// 在堆内存创建了一个对象
  2. Integer i = 817598;</font>
复制代码
… 用如下代码替代:
  1. <font face="微软雅黑">// 仍然留在栈内存中
  2. int i = 817598;</font>
复制代码
当你使用数组的时候事情就变得糟糕了:
  1. <font face="微软雅黑">// 产生了三个堆内存对象
  2. Integer[] i = { 1337, 424242 };</font>
复制代码
… 用如下代码替代:
  1. <font face="微软雅黑">// 只有一个堆内存对象
  2. int[] i = { 1337, 424242 };</font>
复制代码

小结
如果你是在NOPE分支  你就应该非常谨慎使用包装器类型。因为很用可能会JVM的垃圾收集器GC带来很大的压力,不得不让GC耗费不少时间来清理由此带来的垃圾。

比较有效的的一个优化办法是使用原始类型,然后创建一个比较大的一维数组存储这些原始类型的变量,和一些用于标识这些对象在数组中位置的分割变量。

在这里,我推荐使用一个优秀的用于操作原始类型集合的类库,名字叫 trove4j,它比普通的int[]整形数组功能要强大的多,而且它遵循LGPL(宽通用公共许可证,Lesser General Public License)协议。

例外
这条规则有一个例外,就是由于boolean和byte所能表示的数值数目太少,导致不会全部被JDK所缓存。你可以这样写:
  1. <font face="微软雅黑">Boolean a1 = true; // ... syntax sugar for:
  2. Boolean a2 = Boolean.valueOf(true);

  3. Byte b1 = (byte) 123; // ... syntax sugar for:
  4. Byte b2 = Byte.valueOf((byte) 123);</font>
复制代码
这也同样适用于其他整型的原始类型,包括char、short、int、long。
但是只在能对它们进行自动装箱或者调用.valueOf()方法的时候,而在调用构造器时请不要这样使用!

-----------------------------------------------------------------------------------------------------------------------------

6、避免递归
现在,类似Scala这样的函数式编程语言都鼓励使用递归。因为递归通常意味着能分解到单独个体优化的尾递归(tail-recursing)。如果你使用的编程语言能够支持那是再好不过。不过即使如此,也要注意对算法的细微调整将会使尾递归变为普通递归。

希望编译器能自动探测到这一点,否则本来我们将为只需使用几个本地变量就能搞定的事情而白白浪费大量的堆栈框架(stack frames)。

小结
没有太多可说的,除了:当你在N.O.P.E.分支上时,总是要优先考虑迭代而不是递归。

-----------------------------------------------------------------------------------------------------------------------------

7. 使用entrySet( )
当你想要迭代一个Map并且同时需要键和值时,你必须考虑清楚为何要优先使用如下的语句:
  1. <font face="微软雅黑">for (K key : map.keySet()) {
  2.     V value : map.get(key);
  3. }</font>
复制代码
… 而不是这样:
  1. <font face="微软雅黑">for (Entry<K, V> entry : map.entrySet()) {
  2.     K key = entry.getKey();
  3.     V value = entry.getValue();
  4. }</font>
复制代码
当你在N.O.P.E. 分支上时 不管怎样你都应该慎用map,因为对时间复杂度为 O(1)的map的大量的访问操作仍然是很大量的操作,因此也并不是没有成本的。

但是至少在你必须使用map的时候,选择使用entrySet()来迭代它们!反正Map.Entry实例是存在的,你只需要访问它即可。

小结
当你在遍历map的时候如果同时需要键和值,那么请总是优先使用entrySet( )

----------------------------------------------------------------------------------------------------------------------------

8. 使用EnumSet或EnumMap
有些情况下,map中所有可能的键的数量是确定已知的,比如当使用一个用于配置的map时。

如果这个数量相对来说较少,那么你就真的可以考虑使用EnumSet或EnumMap,而不是常见的HashSet或HashMap这一点可以通过EnumMap.put()的源码来解释清楚:
  1. private transient Object[] vals;

  2. public V put(K key, V value) {
  3.     // ...
  4.     int index = key.ordinal();
  5.     vals[index] = maskNull(value);
  6.     // ...
  7. }
复制代码

这个实现的本质是一个事实,我们有一个数组的索引值,而不是哈希表。插入一个新的价值的时候,我们要做的查地图入口问枚举为基础的顺序,它是由每个枚举类型的java编译器生成的。


如果这是一个全球配置图(即只有一个实例),提高访问速度将有助于enummapheavily优于HashMap,这可以用少一点的堆内存,但这必须在每个关键运行hashcode()和equals()。

-----------------------------------------------------------------------------------------------------------------------------

9. 优化你的hashcode()和equals()方法
如果你不能用一个EnumMap,至少优化你的hashcode()和equals()方法。一个好的hashcode()法是重要的因为它会阻止进一步呼吁更昂贵的equals()会产生不同的散列桶每集的情况下。


在每个类层次结构中,可能有受欢迎的简单对象。让我们在jooq的org.jooq.tableimplementations一看。
最简单的,最快可能实现这一hashcode():
  1. // AbstractTable, a common Table base implementation:

  2. @Override
  3. public  int  hashCode ()  { // [#1938] This is a much more efficient hashCode() // implementation compared to that of standard // QueryParts return name. hashCode () ;
  4. }
复制代码

哪里是简单的表名。我们甚至不考虑表的架构或其他属性,因为表名在数据库中通常是足够的。此外,的是一个字符串,所以它已经缓存的hashcode()价值。

评论是很重要的,因为abstracttableextends abstractquerypart,这是任何一个普通基地实施AST(抽象语法树)元。常见的AST元素没有任何属性,所以它不能做任何假设hashcode()实现优化。因此,重写的方法看起来像这样:

  1. // AbstractQueryPart, a common AST element
  2. // base implementation:

  3. @Override
  4. public int hashCode() {
  5.     // This is a working default implementation.
  6.     // It should be overridden by concrete subclasses,
  7.     // to improve performance
  8.     return create().renderInlined(this).hashCode();
  9. }
复制代码

换言之,这将会触发整个的SQL渲染工作流来计算一个AST(普通抽象语法树)元素的哈希值。
equals()方法则变得更有趣:
  1. // AbstractTable, a common Table base implementation:

  2. @Override
  3. public boolean equals(Object that) {
  4.     if (this == that) {
  5.         return true;
  6.     }

  7.     // [#2144] Non-equality can be decided early,
  8.     // without executing the rather expensive
  9.     // implementation of AbstractQueryPart.equals()
  10.     if (that instanceof AbstractTable) {
  11.         if (StringUtils.equals(name,
  12.             (((AbstractTable<?>) that).name))) {
  13.             return super.equals(that);
  14.         }

  15.         return false;
  16.     }

  17.     return false;
  18. }
复制代码
第一件事:总是(不仅在不分)中止每equals()方法的早期,如果:
  • 这个=参数
  • 这种“不相容类型”参数


值得注意的是,后者的情况包括论点= = null,如果你使用的instanceofto检查兼容类型。我们在这之前在10微妙的最佳做法时,java编码。

-----------------------------------------------------------------------------------------------------------------------------

10. 在集合中思考,而不是单个元素
最后但并非最不重要的,有一种东西,不是java相关但不适用于任何语言。此外,我们离开不分支,这个建议可以帮助你从O(N3)为O(n log n),或类似的东西。


不幸的是,许多程序员认为,简单的,本地算法。他们正在逐步解决问题,逐支路,逐环,逐方法。这是命令和/或函数编程风格。虽然它越来越容易模型的“大图片”从纯必须面向对象(仍然命令)功能编程,所有这些风格的东西,只是缺乏SQL和R和类似的语言:

声明式编程
在SQL(我们爱它,因为这是jooq博客)你可以声明你想从你的数据库的结果,没有任何意义可言的算法。然后,数据库可以考虑所有的元数据(如约束,键,索引等),以找出最佳的算法。


在理论上,这是主要的想法背后的SQL和关系演算从一开始。在实践中,SQL厂商实现高效CBO(基于成本的优化器)只有在过去的十年里,所以我们呆在一起在2010年代当SQL最终会发挥其全部潜力(它是关于时间!)

但你也不必考虑集的SQL。设置/集合/袋/列表可在所有语言和图书馆。使用集的主要优点是你的算法会变得更加简洁。写起来容易多了:

有些人可能认为,函数式编程和java 8将帮助您编写更容易,更简洁的算法。那不一定是真的。你可以把你的命令java-7-loop为功能java-8流收集,但你仍然写的非常相同的算法。写一个SQL风格的表达是不同的。这…

-----------------------------------------------------------------------------------------------------------------------------

总结
本文中我们讨论了对  NOPE分支上的优化,重点在对高复杂度算法的优化上。在我们的示例中,要成为  jOOQ  开发者,还要关注优化SQL生成:
  • 每个查询都只用一个StringBuilder 来生成
  • 模板引擎实际上是按字符解析,而不是用正则表达式
  • 尽可能的使用数组,尤其是在遍历**的时候
  • 不要使用JDBC 中非必要的方法
  • 其它…

jOOQ在“食物链的底部”,因为它是用户应用在离开JVM进入DBMS [译者注:数据库管理系统,一般就是指数据库]之前调用的最后(或倒数第二)层API。在食物链的底部就意味着JOOQ的每一行代码都可能被调用  N x O x P  次,所以我们必须加强优化。

原文链接:https://coyee.com/article/11753-top-10-easy-performance-optimisations-in-java

   
关于葡萄城:全球最大的控件提供商,世界领先的企业应用定制工具、企业报表和商业智能解决方案提供商,为超过75%的全球财富500强企业提供服务。

0 个回复

您需要登录后才可以回帖 登录 | 立即注册
返回顶部