过时的Java优化技巧

43

许多与Java性能有关的技巧已经被Java编译器和尤其是Profile-guided optimization所淘汰。例如,这些由平台提供的优化措施可以大幅度(根据来源)降低虚函数调用的成本。VM 还能够进行方法内联、循环展开等操作。

除了上述方法之外,还有哪些性能优化技术您遇到过,但实际上已经被更现代的JVM优化机制所淘汰了呢?


我在这里发布了一个相关问题的悬赏,如果有人能提供答案的引用:https://dev59.com/tm865IYBdhLWcg3wIrFg - andersoj
我认为这个问题存在缺陷,因为它取决于你使用的编译器。 - Raul Lapeira Herrero
@Raul,欢迎提供与编译器有关的信息... - Dan
我在 @Dan 那里搜索了它... 但从实际角度来看,这不是一个失败提示问题,因此没有太多信息可用。 - Raul Lapeira Herrero
8个回答

24

在方法和方法参数上使用的final修饰符并不会对性能产生任何帮助。

此外,Java HotSpot wiki提供了有关HotSpot使用的优化以及如何在Java代码中有效使用它们的良好概述。


2
不错的链接,但似乎不能解除“final”。它说“final”是内联的提示。我认为即使循环调用其他方法并超出当前视野,final修饰符也可以确保编译器其值不会改变。 - Will
请参见:https://dev59.com/RW865IYBdhLWcg3wIrJg - andersoj
4
方法(或类)上的final修饰符不会向JIT提供任何信息。然而,方法参数上的final修饰符可能会提供有用的优化提示(类似于声明为final的局部变量)。但最终使用final的原因是为了强制不可变性-这是设计时的特征,使代码更易于维护。任何优化收益都需要考虑以下问题:a)只有在实际存在性能问题时才会关注它,b)需要进行详尽的测试以确保它确实有所改进。 - Kevin Day
2
实际上,在参数上使用 final 通常不会有任何区别(编译器确实可以看到你是否对其进行赋值,而 Java 不通过引用传递变量 - 对象通过引用传递,但持有它们的变量不是)。但是,这意味着该参数可以与内部类或匿名类一起使用。如果有意义,请使用它,否则不要使用。 - Donal Fellows
4
我使用"final"关键字来确保类的不可变性,通常用于私有的"final"字段。我也使用它来确保不改变传递参数的值或引用。换句话说,我只是为了正确性而使用它,而不是为了优化。 - Archimedes Trajano
6
我不会在方法参数或方法变量上使用final来进行任何优化,而是将其作为编码惯例,以便其他开发人员可以看到该值确实不会改变。任何优化都是次要的。 - Richard

21

有人用多个StringBuilder或StringBuffer调用替换String a = "this" + var1 + " is " + var2;,但实际上Java编译器已经在幕后使用了StringBuilder。


3
哦,我以为它会执行等效于 String a = new StringBuilder("this").append(var1).append("is").append(var2).toString(); 的操作? - Paul Tomblin
3
这个不可靠的来源http://www.rgagnon.com/javadetails/java-0129.html建议保罗在单个StringBuilder方面是正确的,但是它说StringBuilder的默认容量可能太短了(很奇怪,如果编译器需要花时间使用StringBuffer,那么在使用之前计算容量应该是微不足道的)。 - Will
3
当在一行上执行字符串拼接时,使用StringBuilder并没有什么作用。但是,将 String a = "this" + var1; a += " is " + var2; 替换为StringBuilder仍然是有效的。(至少在我最后一次检查时是这样的 :-/) - Devon_C_Miller
2
@Devon,我认为你是对的,因为在多行上执行它会涉及创建一堆不同的字符串对象。 - Paul Tomblin
如果你在循环中执行了数千次,那么这是值得优化的,但是我回答中的示例不会为你节省可测量的时间。 - Paul Tomblin
显示剩余4条评论

17

在开始性能优化之前,有必要定义时间/内存折衷。以下是我为我的内存/时间关键应用程序执行此操作的方式(重复了一些以上的答案,以保证完整):

  1. 规则#1:不要在开发早期进行性能优化。 如果不真正需要它,请不要这样做。如果决定这样做,则:
  2. 使用分析器找到瓶颈,查看源代码找出瓶颈的原因;
  3. 选择适当的数据结构并最好符合定义的时间/内存折衷;
  4. 选择适当的算法(例如迭代与递归等);
  5. 避免使用Java库中的同步对象,如果您不真正需要它;
  6. 避免明确/隐含地创建新对象;
  7. 只有在确定它们不符合您的要求时,才覆盖/重新实现Java提供的数据类型/算法。
  8. 使用小型独立测试来测试所选算法/数据结构的性能。

2
这些提示并不是真的过时了。我的意思是,这是很棒的建议,但你并没有回答问题。 - Charles Goodwin
为了缓解规则1的影响,有些东西很容易编写和阅读,任何一名合格的软件工程师都应该知道并正确执行,例如在链表和数组列表之间进行选择。这是免费的,并且可以避免以后的头痛。这就是优秀的软件设计的实质。 - coffee_machine

8

2001年,我为一款J2ME手机开发了应用程序。它的大小像一块砖头,计算能力也几乎如同砖头。

要让Java应用程序在这款手机上运行得足够流畅,需要尽可能以过程化方式编写代码。此外,最大的性能提升是通过捕获ArrayIndexOutOfBoundsException来退出循环,以遍历向量中的所有项。想想看!

即使在Android上,也有“快速”循环遍历数组中的所有项和“缓慢”的编写相同内容的方式,正如在Google IO视频中提到的dalvik VM内部。

然而,针对你的问题,我认为这种微观优化方式在当今已经非常不寻常了,并且我进一步预计,在JIT VM(甚至是新增JIT的Android 2.2 VM)上,这些优化已经没有意义了。2001年,该手机以33MHz的速度运行KVM解释器。现在,它以500MHz至1500MHz的速度运行dalvik——比KVM快得多的VM——采用更快的ARM架构(即使考虑时钟速度增益也更好),并具有L1等特性,同时还引入了JIT技术。

我们还没有到可以放心在Java中进行直接的像素操作的领域——无论是在手机上还是在i7桌面上——因此仍然存在一些日常代码,Java的速度不够快。 这里有一个有趣的博客,声称一位专家表示,在某些重型CPU任务方面,Java的速度达到了C++速度的80%;我持怀疑态度,因为我编写图像处理代码时,发现Java和本地代码在遍历像素时相差了一个数量级。也许我错过了什么技巧……? :D


1
有点偏离主题,但关于性能方面,我的一个朋友用Java创建了一个源码忠实的Doom移植版本,在单线程P4 @ 3GHz上以640x480分辨率达到约130-145 FPS。不是OpenGL,只是blitting。 - Tassos Bassoukos
是的,但我在486上玩了《毁灭战士》...(不过,你朋友的项目听起来非常有趣和很酷) - Will

4
  1. 不要手动调用垃圾收集器,这会影响现代JVM实现的性能。
  2. 使用Integer而不是Long并不能节省太多空间,但会限制数字的范围。
  3. 避免手动生成枚举类,使用内置的Enum。Java 1.5引入了真正的枚举类型,应该使用它们。

2

当使用小于32GB RAM的x64 JVM时:

与32位JVM相比,64位JVM由于较大的普通对象指针而使用30%-50%更多的内存。您可以通过使用JDK6+来大幅减少此因素。

从JDK6u6p到JDK6u22,它是可选的,并且可以通过添加JVM参数来启用:

-XX:+UseCompressedOops 

从JDK6u23开始(JDK7同样如此),它已默认启用。更多信息在这里


1
  1. "过早的优化是万恶之源" (Donald Knuth)
  2. 只有优化瓶颈才有用。
  3. 在每种情况下都应该分析代码。也许你可以用快速的 HashSet 替换 TreeSet,因为你不需要排序功能,或者你可以使用 float 而不是 double(查看 Android SDK)。
  4. 如果没有任何技术帮助,你可以尝试重写一段代码并通过 JNI 调用它,以便使用本地代码。

7
唐纳德·科恩(Donald Knuth)曾说过:“我们应该忘记小的效率问题,大约有97%的时间:过早优化是所有罪恶的根源。”但是这句引用既不完整,也与问题无关。其次,这个问题是关于一般优化技巧的,这些技巧已经不再适用了,没有什么可以证明它是过早的,这是一个普遍讨论。 - hhafez

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接