在循环内部或外部声明变量

Har*_*Joy 224 java optimization while-loop

为什么以下工作正常?

String str;
while (condition) {
    str = calculateStr();
    .....
}
Run Code Online (Sandbox Code Playgroud)

但据说这个是危险的/不正确的:

while (condition) {
    String str = calculateStr();
    .....
}
Run Code Online (Sandbox Code Playgroud)

是否有必要在循环外声明变量?

Pri*_*osK 286

我比较了这两个(类似)示例的字节代码:

我们来看看1.例子:

package inside;

public class Test {
    public static void main(String[] args) {
        while(true){
            String str = String.valueOf(System.currentTimeMillis());
            System.out.println(str);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

之后javac Test.java,javap -c Test你会得到:

public class inside.Test extends java.lang.Object{
public inside.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   3:   invokestatic    #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
   6:   astore_1
   7:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   14:  goto    0

}
Run Code Online (Sandbox Code Playgroud)

我们来看看2.例子:

package outside;

public class Test {
    public static void main(String[] args) {
        String str;
        while(true){
            str =  String.valueOf(System.currentTimeMillis());
            System.out.println(str);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

之后javac Test.java,javap -c Test你会得到:

public class outside.Test extends java.lang.Object{
public outside.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   3:   invokestatic    #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
   6:   astore_1
   7:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   14:  goto    0

}
Run Code Online (Sandbox Code Playgroud)

观察结果表明,这两个例子没有区别.这是JVM规范的结果......

但是在最佳编码实践的名义下,建议在尽可能小的范围内声明变量(在此示例中,它位于循环内部,因为这是使用变量的唯一位置).

  • 对于你们中的任何一个"最终"爱好者:在`inside`包中声明`str`为`final`****也没有区别=) (7认同)
  • 它是JVM Soecification的结果,而不是"编译器优化".方法所需的堆栈槽都在进入方法时分配.这就是字节码的指定方式. (3认同)
  • @Arhimed还有一个理由将它放在循环中(或只是'{}'块):如果你在另一个范围内声明一些变量,编译器将重用在另一个范围内的变量的堆栈帧中分配的内存. (2认同)

Mik*_*kis 279

局部变量的范围应始终尽可能小.

在您的例子我相信str不会在外部使用while循环,否则你就不会问这个问题,因为它声明的内部while循环不会是一个选项,因为它不会编译.

所以,既然str使用外循环,在尽可能小的范围str while循环.

所以,答案是着重str绝对应该被while循环内声明.没有ifs,没有ands,没有buts.

唯一可能违反此规则的情况是,由于某种原因,必须从代码中挤出每个时钟周期至关重要,在这种情况下,您可能需要考虑在外部作用域中实例化某些内容并重新使用它而不是在内部范围的每次迭代中重新实例化它.但是,由于java中字符串的不变性,这不适用于您的示例:str的新实例将始终在循环的开头创建,并且必须在其结尾处丢弃,因此没有可能优化那里.

编辑:(在答案中注入我的评论)

在任何情况下,正确的做法是正确编写所有代码,为产品建立性能要求,根据此要求测量最终产品,如果不满足,则优化.通常最终会发生的事情是,您可以找到方法在几个地方提供一些很好的和正式的算法优化,使我们的程序满足其性能要求,而不必遍及整个代码库,调整和破解为了在这里和那里挤压时钟周期.

  • @HarryJoy正确的做法是编写所有代码_properly_,为您的产品建立性能要求,根据此要求测量最终产品,如果不满足,则进行优化.你知道吗?您通常可以在几个地方提供一些不错的和正式的算法优化,这些优化可以完成这一操作,而不必遍布整个代码库并进行调整和破解,以便在这里和那里挤压时钟周期. (6认同)
  • 您可以看到,现代的多千兆赫,多核,流水线,多级内存缓存CPU使我们能够专注于遵循最佳实践,而无需担心时钟周期.此外,优化仅适用于_if且仅当已经确定有必要时,并且在必要时,一些高度本地化的调整通常会实现所需的性能,因此不需要乱丢所有代码以性能为名的小黑客. (5认同)
  • 查询最后一段:如果是另一个那么String不是不可变的那么它会影响吗? (2认同)
  • @HarryJoy 是的,当然,以可变的 StringBuilder 为例。如果您使用 StringBuilder 在循环的每次迭代中构建一个新字符串,那么您可以通过在循环外分配 StringBuilder 来优化事物。但是,这仍然不是一种可取的做法。如果你没有一个很好的理由就这样做,那就是过早的优化。 (2认同)
  • @MikeNakis我在非常狭窄的范围内思考的事情. (2认同)

Cha*_*har 26

声明最小范围内的对象可提高可读性.

对于今天的编译器而言,性能并不重要.(在这种情况下)
从维护的角度来看,第二种选择更好.
在尽可能最窄的范围内在同一位置声明和初始化变量.

正如唐纳德·欧文·克努特所说:

"我们应该忘记小的效率,大约97%的时间说:过早的优化是所有邪恶的根源"

ie)程序员让性能考虑影响一段代码设计的情况.这可能导致设计不像以前那样干净代码不正确,因为优化会使代码变得复杂,并且程序员会因优化而分散注意力.


Azo*_*ous 12

如果你也想使用str外面的looop; 在外面宣布它.否则,第二版很好.


Onu*_*uru 9

请跳到更新的答案...

对于那些关心性能的人,请取出System.out并将循环限制为1个字节.使用double(测试1/2)和使用String(3/4),下面给出了Windows 7 Professional 64位和JDK-1.7.0_21的经过时间(以毫秒为单位).字节码(下面也给出了test1和test2)并不相同.我懒得用可变的和相对复杂的物体进行测试.

Test1拍摄:2710毫秒

Test2拍摄:2790毫秒

字符串(在测试中只用字符串替换double)

Test3采用:1200毫秒

Test4采用:3000毫秒

编译并获取字节码

javac.exe LocalTest1.java

javap.exe -c LocalTest1 > LocalTest1.bc


public class LocalTest1 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        double test;
        for (double i = 0; i < 1000000000; i++) {
            test = i;
        }
        long finish = System.currentTimeMillis();
        System.out.println("Test1 Took: " + (finish - start) + " msecs");
    }

}

public class LocalTest2 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        for (double i = 0; i < 1000000000; i++) {
            double test = i;
        }
        long finish = System.currentTimeMillis();
        System.out.println("Test1 Took: " + (finish - start) + " msecs");
    }
}


Compiled from "LocalTest1.java"
public class LocalTest1 {
  public LocalTest1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: dconst_0
       5: dstore        5
       7: dload         5
       9: ldc2_w        #3                  // double 1.0E9d
      12: dcmpg
      13: ifge          28
      16: dload         5
      18: dstore_3
      19: dload         5
      21: dconst_1
      22: dadd
      23: dstore        5
      25: goto          7
      28: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      31: lstore        5
      33: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      36: new           #6                  // class java/lang/StringBuilder
      39: dup
      40: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      43: ldc           #8                  // String Test1 Took:
      45: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      48: lload         5
      50: lload_1
      51: lsub
      52: invokevirtual #10                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      55: ldc           #11                 // String  msecs
      57: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      60: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      63: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      66: return
}


Compiled from "LocalTest2.java"
public class LocalTest2 {
  public LocalTest2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: dconst_0
       5: dstore_3
       6: dload_3
       7: ldc2_w        #3                  // double 1.0E9d
      10: dcmpg
      11: ifge          24
      14: dload_3
      15: dstore        5
      17: dload_3
      18: dconst_1
      19: dadd
      20: dstore_3
      21: goto          6
      24: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      27: lstore_3
      28: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      31: new           #6                  // class java/lang/StringBuilder
      34: dup
      35: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      38: ldc           #8                  // String Test1 Took:
      40: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      43: lload_3
      44: lload_1
      45: lsub
      46: invokevirtual #10                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      49: ldc           #11                 // String  msecs
      51: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      54: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      57: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      60: return
}
Run Code Online (Sandbox Code Playgroud)

更新的答案

将性能与所有JVM优化进行比较真的不容易.但是,这有点可能.Google Caliper中更好的测试和详细结果

  1. 关于博客的一些细节:你应该在循环内或循环之前声明一个变量吗?
  2. GitHub存储库:https://github.com/gunduru/jvdt
  3. 双案例和100M循环的测试结果(以及所有JVM详细信息):https://microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4

宣布在1,759.209之前宣布在里面2,242.308

  • 在1,759.209 ns之前宣布
  • DeclaredInside 2,242.308 ns

双重声明的部分测试代码

这与上面的代码不同.如果你只编写一个虚拟循环JVM跳过它,那么至少你需要分配并返回一些东西.在Caliper文档中也建议这样做.

@Param int size; // Set automatically by framework, provided in the Main
/**
* Variable is declared inside the loop.
*
* @param reps
* @return
*/
public double timeDeclaredInside(int reps) {
    /* Dummy variable needed to workaround smart JVM */
    double dummy = 0;

    /* Test loop */
    for (double i = 0; i <= size; i++) {

        /* Declaration and assignment */
        double test = i;

        /* Dummy assignment to fake JVM */
        if(i == size) {
            dummy = test;
        }
    }
    return dummy;
}

/**
* Variable is declared before the loop.
*
* @param reps
* @return
*/
public double timeDeclaredBefore(int reps) {

    /* Dummy variable needed to workaround smart JVM */
    double dummy = 0;

    /* Actual test variable */
    double test = 0;

    /* Test loop */
    for (double i = 0; i <= size; i++) {

        /* Assignment */
        test = i;

        /* Not actually needed here, but we need consistent performance results */
        if(i == size) {
            dummy = test;
        }
    }
    return dummy;
}
Run Code Online (Sandbox Code Playgroud)

总结:declaredBefore表示性能更好 - 非常小 - 并且它违反了最小范围原则.JVM实际上应该为你做这件事

  • 1)Java字节码在运行时得到优化(重新排序,折叠等),所以不要过多关注.class文件中写的内容.2)有1.000.000.000次运行以获得2.8s的性能胜利,因此每次运行大约2.8ns与安全和正确的编程风格相比.对我来说是一个明显的赢家 3)由于您没有提供有关预热的信息,因此您的计时毫无用处. (2认同)

Jan*_*yka 7

在内部,变量可见的范围越小越好.


小智 7

解决此问题的一个方法是提供封装while循环的变量范围:

{
  // all tmp loop variables here ....
  // ....
  String str;
  while(condition){
      str = calculateStr();
      .....
  }
}
Run Code Online (Sandbox Code Playgroud)

当外部范围结束时,它们将自动取消引用.


Cra*_*lus 6

如果你不需要使用strwhile循环(范围相关),那么第二个条件即

  while(condition){
        String str = calculateStr();
        .....
    }
Run Code Online (Sandbox Code Playgroud)

更好,因为如果你在堆栈上定义一个对象,只有它condition是真的.即如果你需要它也可以使用

  • 请注意,即使在第一个变体中,如果条件为假,也不会构造任何对象. (2认同)