【问题标题】:Declaring variables inside or outside of a loop在循环内部或外部声明变量
【发布时间】:2012-02-06 21:19:05
【问题描述】:

为什么以下工作正常?

String str;
while (condition) {
    str = calculateStr();
    .....
}

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

while (condition) {
    String str = calculateStr();
    .....
}

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

【问题讨论】:

    标签: java optimization while-loop


    【解决方案1】:

    局部变量的范围应该总是尽可能小。

    在您的示例中,我假设 strnotwhile 循环之外使用的,否则您不会问这个问题,因为在 while 循环内声明它不会是一个选项,因为它不会编译。

    因此,由于str在循环外使用,str 的最小可能范围是 while 循环内。

    所以,答案是强调str 绝对应该在while 循环中声明。没有如果,没有与,没有但是。

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

    编辑:(在答案下方插入我的评论)

    无论如何,正确的做事方式是正确编写所有代码,为产品建立性能要求,根据此要求衡量最终产品,如果不满足,则进行优化。通常最终会发生的事情是,您找到了在几个地方提供一些不错的正式算法优化的方法,这使我们的程序满足其性能要求,而不必遍历整个代码库并调整和破解。为了在这里和那里压缩时钟周期。

    【讨论】:

    • 查询最后一段:如果是另一个字符串,那么它不是不可变的,那么它会影响吗?
    • @HarryJoy 是的,当然,以 StringBuilder 为例,它是可变的。如果您使用 StringBuilder 在循环的每次迭代中构建一个新字符串,那么您可以通过在循环外分配 StringBuilder 来优化事情。但是,这仍然不是一个可取的做法。如果你没有很好的理由这样做,那就是过早的优化。
    • @HarryJoy 做事的正确方法是正确编写所有代码,为您的产品建立性能要求,根据此要求衡量您的最终产品,如果它不满足,那就去优化吧。你知道吗?您通常可以在几个地方提供一些不错的正式算法优化,这样就可以解决问题,而不必遍历整个代码库并调整和修改一些东西以便在这里和那里压缩时钟周期。
    • @MikeNakis 我认为你的想法范围很窄。
    • 你看,现代多千兆赫、多核、流水线、多级内存缓存 CPU 使我们能够专注于遵循最佳实践,而不必担心时钟周期。此外,仅当当且仅当确定有必要时,才建议进行优化,并且在必要时,一些高度本地化的调整通常会达到所需的性能,因此不需要以性能的名义在我们所有的代码中乱扔垃圾。
    【解决方案2】:

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

    让我们看看1。示例

    package inside;
    
    public class Test {
        public static void main(String[] args) {
            while(true){
                String str = String.valueOf(System.currentTimeMillis());
                System.out.println(str);
            }
        }
    }
    

    javac Test.javajavap -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
    
    }
    

    让我们看看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);
            }
        }
    }
    

    javac Test.javajavap -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
    
    }
    

    观察表明,这两个示例之间没有区别。这是JVM规范的结果……

    但以最佳编码实践的名义,建议在尽可能小的范围内声明变量(在本例中,它位于循环内,因为这是唯一使用变量的地方)。

    【讨论】:

    • 这是JVM Soecification的结果,而不是“编译器优化”。方法所需的堆栈槽全部在进入该方法时分配。这就是字节码的指定方式。
    • @Arhimed 还有一个理由将它放在循环中(或只是'{}'块):如果你在另一个范围内,编译器将重用堆栈帧中分配的内存用于变量在其他范围内声明一些过度变量。
    • 如果它通过数据对象列表循环,那么它会对大量数据产生任何影响吗?大概有 4 万。
    • 对于你们中的任何一个final 爱好者:在inside 包案例中将str 声明为final 没有区别=)
    【解决方案3】:

    最小范围中声明对象可以提高可读性

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

    正如 Donald Ervin Knuth 所说:

    “我们应该忘记小的效率,比如说大约 97% 的时间: 过早优化是万恶之源”

    i.e) 程序员让性能考虑影响设计一段代码的情况。这可能会导致设计不像它可能的那样干净不正确的代码,因为代码被复杂 优化,程序员被优化分心了。

    【讨论】:

    • “第二个选项的性能稍快一些” =>你测量过吗?根据其中一个答案,字节码是相同的,所以我看不出性能会有什么不同。
    • 很抱歉,但这确实不是测试 java 程序性能的正确方法(你怎么能测试无限循环的性能呢?)
    • 我同意你的其他观点——只是我相信没有性能差异。
    【解决方案4】:

    如果你也想在循环外使用str;在外面声明。否则,第二个版本就可以了。

    【讨论】:

      【解决方案5】:

      请跳到更新的答案...

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

      双倍

      Test1 耗时:2710 毫秒

      Test2 耗时:2790 毫秒

      字符串(只是在测试中用字符串替换双精度)

      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
      }
      

      更新答案

      将性能与所有 JVM 优化进行比较确实不容易。不过,这多少有些可能。 Google Caliper中更好的测试和详细结果

      1. 博客的一些细节:Should you declare a variable inside a loop or before the loop?
      2. GitHub 存储库:https://github.com/gunduru/jvdt
      3. 双重情况和 100M 循环的测试结果(是的,所有 JVM 详细信息):https://microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4

      • 在 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;
      }
      

      总结:declaredBefore 表示更好的性能 - 非常小 - 并且它违反了最小范围原则。 JVM 实际上应该为您执行此操作

      【讨论】:

      • 测试方法无效,并且您没有对结果提供任何解释。
      • @EJP 这对于那些对该主题感兴趣的人来说应该很清楚。方法取自 PrimosK 的回答,以提供更多有用的信息。老实说我不知道​​如何改进这个答案,也许你可以点击编辑并向我们展示如何正确地做到这一点?
      • 1) Java 字节码在运行时得到优化(重新排序、折叠等),所以不要太在意 .class 文件中写入的内容。 2) 有 1.000.000.000 次运行以获得 2.8 秒的性能优势,因此与安全和正确的编程风格相比,每次运行大约 2.8 纳秒。对我来说是一个明显的赢家。 3)由于您没有提供有关热身的信息,因此您的时间安排毫无用处。
      • @Hardcoded 更好的测试/微基准测试,仅用于双倍和 100M 循环。在线结果,如果您想要其他案例,请随时编辑。
      • 谢谢,这消除了第 1) 和第 3) 点。但即使时间上升到每个周期约 5ns,这仍然是一个可以忽略的时间。理论上有一个小的优化潜力,实际上你每个周期所做的事情通常要贵得多。因此,在几分钟甚至几小时的运行中,潜力最多只有几秒钟。在花时间进行这种低级优化之前,我会检查其他具有更高潜力的选项(例如 Fork/Join、并行流)。
      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

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

        {
          // all tmp loop variables here ....
          // ....
          String str;
          while(condition){
              str = calculateStr();
              .....
          }
        }
        

        当外部作用域结束时,它们会自动取消引用。

        【讨论】:

          【解决方案8】:

          如果您不需要在 while 循环之后使用 str(范围相关),那么第二个条件即

            while(condition){
                  String str = calculateStr();
                  .....
              }
          

          更好,因为如果您仅在 condition 为真时在堆栈上定义对象。 IE。 如果需要,请使用它

          【讨论】:

          • 请注意,即使在第一个变体中,如果条件为假,则不会构造任何对象。
          • @菲利普:是的,你是对的。我的错。我一直在想现在的样子。你觉得呢?
          • 好吧,“在堆栈上定义一个对象”在 Java 世界中是一个有点奇怪的术语。此外,在堆栈上分配一个变量通常在运行时是一个 noop,那么为什么要麻烦呢?确定范围以帮助程序员是真正的问题。
          【解决方案9】:

          我认为回答您问题的最佳资源是以下帖子:

          Difference between declaring variables before or in loop?

          据我了解,这将取决于语言。 IIRC Java 对此进行了优化,因此没有任何区别,但 JavaScript(例如)每次都会在循环中完成整个内存分配。特别是在 Java 中,我认为第二个在完成分析时会运行得更快。

          【讨论】:

            【解决方案10】:

            变量的声明应尽可能靠近它们的使用位置。

            它使 RAII (Resource Acquisition Is Initialization) 更容易。

            它使变量的范围保持紧密。这可以让优化器更好地工作。

            【讨论】:

              【解决方案11】:

              根据 Google Android Development guide,变量范围应该是有限的。请检查此链接:

              Limit Variable Scope

              【讨论】:

                【解决方案12】:

                while 循环之外声明字符串str 允许在while 循环内部和外部引用它。在 while 循环内声明字符串 str 允许它while 循环内被引用。

                【讨论】:

                  【解决方案13】:

                  正如许多人指出的那样,

                  String str;
                  while(condition){
                      str = calculateStr();
                      .....
                  }
                  

                  比这更好:

                  while(condition){
                      String str = calculateStr();
                      .....
                  }
                  

                  因此,如果您不重用变量,请不要在其范围之外声明变量...

                  【讨论】:

                  • 除了可能这样:link
                  【解决方案14】:

                  在循环内声明会限制相应变量的范围。这完全取决于项目对变量范围的要求。

                  【讨论】:

                    【解决方案15】:

                    确实,上述问题是一个编程问题。您想如何编写代码?您需要在哪里访问“STR”?声明一个在本地用作全局变量的变量是没有用的。我相信的编程基础。

                    【讨论】:

                      【解决方案16】:

                      str 变量将可用并在内存中保留一些空间,即使在下面的代码执行之后也是如此。

                       String str;
                          while(condition){
                              str = calculateStr();
                              .....
                          }
                      

                      str 变量将不可用,并且在下面的代码中为str 变量分配的内存将被释放。

                      while(condition){
                          String str = calculateStr();
                          .....
                      }
                      

                      如果我们确实遵循第二个,这将减少我们的系统内存并提高性能。

                      【讨论】:

                        【解决方案17】:

                        这两个例子的结果是一样的。但是,第一个为您提供了在 while 循环之外使用 str 变量;第二个不是。

                        【讨论】:

                          【解决方案18】:

                          我认为对象的大小也很重要。 在我的一个项目中,我们声明并初始化了一个大型二维数组,该数组使应用程序抛出内存不足异常。 我们将声明移出循环,并在每次迭代开始时清除数组。

                          【讨论】:

                            【解决方案19】:

                            对这个问题中几乎每个人的警告:这是示例代码,在循环内部,使用Java 7 在我的计算机上它可以很容易地慢 200 倍(并且内存消耗也略有不同)。但它与分配有关,而不仅仅是范围。

                            public class Test
                            {
                                private final static int STUFF_SIZE = 512;
                                private final static long LOOP = 10000000l;
                            
                                private static class Foo
                                {
                                    private long[] bigStuff = new long[STUFF_SIZE];
                            
                                    public Foo(long value)
                                    {
                                        setValue(value);
                                    }
                            
                                    public void setValue(long value)
                                    {
                                        // Putting value in a random place.
                                        bigStuff[(int) (value % STUFF_SIZE)] = value;
                                    }
                            
                                    public long getValue()
                                    {
                                        // Retrieving whatever value.
                                        return bigStuff[STUFF_SIZE / 2];
                                    }
                                }
                            
                                public static long test1()
                                {
                                    long total = 0;
                            
                                    for (long i = 0; i < LOOP; i++)
                                    {
                                        Foo foo = new Foo(i);
                                        total += foo.getValue();
                                    }
                            
                                    return total;
                                }
                            
                                public static long test2()
                                {
                                    long total = 0;
                            
                                    Foo foo = new Foo(0);
                                    for (long i = 0; i < LOOP; i++)
                                    {
                                        foo.setValue(i);
                                        total += foo.getValue();
                                    }
                            
                                    return total;
                                }
                            
                                public static void main(String[] args)
                                {
                                    long start;
                            
                                    start = System.currentTimeMillis();
                                    test1();
                                    System.out.println(System.currentTimeMillis() - start);
                            
                                    start = System.currentTimeMillis();
                                    test2();
                                    System.out.println(System.currentTimeMillis() - start);
                                }
                            }
                            

                            结论:根据局部变量的大小,差异可能很大,即使变量不是很大。

                            只是说有时候,循环外部或内部确实很重要。

                            【讨论】:

                            • 当然,第二个更快,但你在做不同的事情:test1 正在创建很多带有大数组的 Foo-Objects,而 test2 不是。 test2 一遍又一遍地重用同一个 Foo 对象,这在多线程环境中可能很危险。
                            • 多线程环境危险???请解释原因。我们正在谈论一个局部变量。它在每次调用该方法时创建。
                            • 如果您将 Foo-Object 传递给异步处理数据的操作,则当您更改其中的数据时,该操作可能仍在 Foo-instance 上运行。它甚至不必是多线程的就可以产生副作用。所以实例重用是相当危险的,当你不知道谁还在使用实例时
                            • Ps:你的setValue方法应该是bigStuff[(int) (value % STUFF_SIZE)] = value;(试试2147483649L的值)
                            • 谈到副作用:您是否比较了您的方法的结果?
                            【解决方案20】:

                            如果您的calculateStr() 方法返回null,然后您尝试调用str 上的方法,您就有NullPointerException 的风险。

                            更一般地,避免使用具有 null 值的变量。顺便说一句,它对类属性更强。

                            【讨论】:

                            • 这与问题无关。 NullPointerException(在未来的函数调用中)的概率不取决于变量的声明方式。
                            • 我不这么认为,因为问题是“最好的方法是什么?”。恕我直言,我更喜欢更安全的代码。
                            • NullPointerException. 的风险为零如果此代码尝试return str;,则会遇到编译错误。
                            猜你喜欢
                            • 2016-12-05
                            • 1970-01-01
                            • 2011-04-10
                            • 1970-01-01
                            • 2014-02-16
                            • 2010-12-25
                            • 2010-09-27
                            • 2014-03-24
                            相关资源
                            最近更新 更多