Java代码优化,它将优化这个问题吗?

3

随着较新版本的编译器推出,我发现自己试图编写更易于阅读但可能更占内存的代码,如果我所期望的优化没有被执行,那么这种做法就可能会造成问题。 以这段代码为例,非常简单:

while (scanner.hasNextLine() && !result)
{
    String line = scanner.nextLine();
    result = line.indexOf(searchString) >= 0;
}

假设使用的是Eclipse Juno和Java 7,那么可以合理地推断出这将会生成与之前相同的字节码。

while (scanner.hasNextLine() && !result)
{
    result = scanner.nextLine().indexOf(searchString) >= 0;
}

虽然前者只有两行代码,但它缩短了第二行的长度,使得阅读更加方便。个人认为... 但这样做是否会导致创建不必要的字符串对象呢?希望不会...


1
两个版本中对象的创建数量是相同的。 - Denys Séguret
那段代码在性能方面几乎是相等的。 - Esailija
4
早期优化是万恶之源。 - ppeterka
2
使用JDK附带的javap工具自行查找。使用-c选项反汇编两个版本的代码。如果两个版本的字节码完全相同,我不会感到惊讶。(事实上,如果它们不同,我会感到惊讶)。 - Jesper
1
@ppeterka 错了。过早优化才是。早期优化有时是完全有效和高效的,可以节省后来大量的优化工作。 - user395760
显示剩余10条评论
6个回答

7
您无法避免创建字符串。您将其分配给本地变量的事实在这里是无关紧要的,实际上在字节码级别上,这个事实甚至不会被注意到:在该级别上,有或没有显式变量,结果的引用必须放置在堆栈上,以便传递到链中的下一个方法调用中。
您认为创建不必要的字符串实例的想法可能源于其他语言(如C)的本能反应:赋值String s = ...仅复制对唯一字符串实例的引用。这是因为所有Java对象都驻留在堆上,因此您总是需要显式复制对象才能实际涉及另一个实例。例如,如果您编写了String line = new String(scanner.nextLine()),那确实会创建一个不必要的String实例。
最后,任何版本的代码都没有优化,因此只根据风格偏好进行选择。

这个例子可能过于简化了,但我理解Marko在这里的观点。我在SO上读到过一篇帖子(我无法追踪),大多数情况下,如果你不太关注尝试编写复杂语句来优化代码,当现代编译器解析你的代码并寻找可以轻松优化的常见编码结构时,编译后的代码将会更好。谢谢。 - MayoMan
首先要区分的是Java编译器和JIT编译器。前者只是一个工作马,做一些样板文件的事情,所有有趣的事情都发生在JIT中。因此,JIT甚至不能直接看到您的源代码。这使得试图聪明地超越系统变得更加徒劳,因为该系统旨在为惯用的Java编写产生最佳结果。 - Marko Topolnik

7
一些通用原则:
- 过早优化是毫无意义的。 - 在这些小情况下为可读性而节省也是没有意义的。 - 大多数情况下,优化来自于改变算法及其复杂度。 - 当尝试优化非算法时,你永远不会像编译器一样擅长此事。 - 确定任何优化只有两种方法:查看字节码或基准测试性能,其他所有内容通常都是猜测。
在您的特定情况中:变量声明在优化方面并没有改变什么,因为在两种情况下,都是通过 `nextLine()` 实例化字符串并将其放置在堆栈上,将其分配给变量(除非它是实例变量,因为它的有用性仅限于您的视觉),并不会改变任何东西。

2

为什么不询问Java类文件反汇编器 - 包含在每个JDK中的javap程序呢?

有以下源代码:

public class Foo {

    static void m1(Scanner scanner, String searchString, boolean result) {
        while (scanner.hasNextLine() && !result) {
            String line = scanner.nextLine();
            result = line.indexOf(searchString) >= 0;
        }
    }

    static void m2(Scanner scanner, String searchString, boolean result) {
        while (scanner.hasNextLine() && !result) {
            result = scanner.nextLine().indexOf(searchString) >= 0;
        }
    }
}

运行反汇编程序时:

javap -c Foo.class

您得到以下字节码:
static void m1(java.util.Scanner, java.lang.String, boolean);
Code:
   0: goto          22
   3: aload_0
   4: invokevirtual #33                 // Method java/util/Scanner.nextLine:()Ljava/lang/String;
   7: astore_3
   8: aload_3
   9: aload_1
  10: invokevirtual #39                 // Method java/lang/String.indexOf:(Ljava/lang/String;)I
  13: iflt          20
  16: iconst_1
  17: goto          21
  20: iconst_0
  21: istore_2
  22: aload_0
  23: invokevirtual #45                 // Method java/util/Scanner.hasNextLine:()Z
  26: ifeq          33
  29: iload_2
  30: ifeq          3
  33: return

static void m2(java.util.Scanner, java.lang.String, boolean);
Code:
   0: goto          20
   3: aload_0
   4: invokevirtual #33                 // Method java/util/Scanner.nextLine:()Ljava/lang/String;
   7: aload_1
   8: invokevirtual #39                 // Method java/lang/String.indexOf:(Ljava/lang/String;)I
  11: iflt          18
  14: iconst_1
  15: goto          19
  18: iconst_0
  19: istore_2
  20: aload_0
  21: invokevirtual #45                 // Method java/util/Scanner.hasNextLine:()Z
  24: ifeq          31
  27: iload_2
  28: ifeq          3
  31: return

如果您比较这两种方法的字节码,您会发现唯一的区别就是m1包含了这两个额外的指令:

7: astore_3
8: aload_3

这只是将堆栈顶部的对象引用存储到本地变量中,没有其他操作。
编辑:
反汇编器还可以显示方法的本地变量数量:
javap -l Foo.class

这将输出:

static void m1(java.util.Scanner, java.lang.String, boolean);
LocalVariableTable:
  Start  Length  Slot  Name   Signature
         0      34     0 scanner   Ljava/util/Scanner;
         0      34     1 searchString   Ljava/lang/String;
         0      34     2 result   Z
         8      14     3  line   Ljava/lang/String;

static void m2(java.util.Scanner, java.lang.String, boolean);
LocalVariableTable:
  Start  Length  Slot  Name   Signature
         0      32     0 scanner   Ljava/util/Scanner;
         0      32     1 searchString   Ljava/lang/String;
         0      32     2 result   Z
}

基本上,以上所见的唯一差异得到了证实 - m1 方法仅分配了一个更多的局部变量 - String line它不会创建任何其他对象,它只会创建一个对无论如何都分配的对象的另一个引用


可能会在运行时进行优化的操作(感谢 JIT)。 - Colin Hebert

2

看起来离题了,但这也会提高性能。

while (!result && scanner.hasNextLine())
{
    String line = scanner.nextLine();
    result = line.indexOf(searchString) >= 0;
}

1

回答你的问题,当执行 scanner.nextLine().indexOf(searchString) 时,你期望 nextLine() 做什么?你期望在哪个对象上执行 indexOf()

正如你可能猜到的那样,它依赖于一个 String;所以是的,这个 String 被创建并且被使用了。与你的猜测相反,这是必要的

声明一个变量 (String s) 并赋值的操作与实例化一个对象 (new String("test")) 相比几乎不耗费任何成本。

换句话说,你试图实现的东西既没有用处,也没有显著的性能提升。


这里有第二个问题,对于开发人员来说是一个更深层次的问题。试图在没有遇到实际问题且没有任何明显迹象表明此代码会使应用程序运行显著变慢的情况下对这种代码进行优化,就是过早地进行了优化。
大多数时候,这会让你分心,导致你为了优化(甚至可能根本不是优化!)而编写难以阅读的代码,从而使你无法达成想要实现的目标。
在您的特定情况下(我很惊讶之前没有人提到它,这就是我写这篇答案的原因),您的“优化”代码将使每个人的生活更加糟糕。
想象一下,您的代码正在运行,某个时刻一切都失败了,并且您在这行代码上收到了一个NullPointerException:
    result = scanner.nextLine().indexOf(searchString) >= 0;

与其清楚地知道现在出了什么问题,你现在不得不手动调试代码,以查找scanner是否为null,或者由于某种原因nextLine()返回null

这个问题甚至在你之前的代码中都不存在,但是这种早期优化和尝试使你的代码更紧凑以避免浪费一些操作的愿望现在已经使你的代码全局变差。


0
编译器将无论如何内联本地变量行,因此两者之间没有区别。

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