【问题标题】:Difference between declaring variables before or in loop?在循环之前或循环中声明变量之间的区别?
【发布时间】:2010-09-29 06:23:28
【问题描述】:

我一直想知道,一般来说,在循环之前声明一个丢弃变量,而不是在循环内重复声明,是否会产生任何(性能)​​差异? Java中的一个(非常没有意义)示例:

a) 循环前声明:

double intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

b) 声明(重复)在循环内:

for(int i=0; i < 1000; i++){
    double intermediateResult = i;
    System.out.println(intermediateResult);
}

a 还是 b 哪个更好?

我怀疑重复的变量声明(例如 b)会产生更多的开销理论上,但编译器足够聪明,所以这并不重要。示例 b 的优点是更紧凑,并将变量的范围限制在使用它的位置。尽管如此,我还是倾向于根据示例 a 编写代码。

编辑:我对 Java 案例特别感兴趣。

【问题讨论】:

  • 这在为 Android 平台编写 Java 代码时很重要。谷歌建议,对于时间要求严格的代码,在 for 循环之外声明递增变量,就像在 for 循环内部一样,它每次在该环境中都重新声明它。对于昂贵的算法,性能差异非常明显。
  • @AaronCarson 您能否提供指向此 Google 建议的链接

标签: java performance loops variables initialization


【解决方案1】:

当我想在退出循环后查看变量的内容时,我使用 (A)。它只对调试很重要。当我希望代码更紧凑时,我使用 (B),因为它节省了一行代码。

【讨论】:

    【解决方案2】:

    很长一段时间以来,我一直有同样的问题。所以我测试了一段更简单的代码。

    结论:对于这种情况存在NO性能差异。

    外循环情况

    int intermediateResult;
    for(int i=0; i < 1000; i++){
        intermediateResult = i+2;
        System.out.println(intermediateResult);
    }
    

    内循环案例

    for(int i=0; i < 1000; i++){
        int intermediateResult = i+2;
        System.out.println(intermediateResult);
    }
    

    我在 IntelliJ 的反编译器上检查了编译后的文件,对于这两种情况,我得到了 same Test.class

    for(int i = 0; i < 1000; ++i) {
        int intermediateResult = i + 2;
        System.out.println(intermediateResult);
    }
    

    我还使用answer 中给出的方法对这两种情况的代码进行了反汇编。我将只显示与答案相关的部分

    外循环情况

    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_2
         2: iload_2
         3: sipush        1000
         6: if_icmpge     26
         9: iload_2
        10: iconst_2
        11: iadd
        12: istore_1
        13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: iload_1
        17: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        20: iinc          2, 1
        23: goto          2
        26: return
    LocalVariableTable:
            Start  Length  Slot  Name   Signature
               13      13     1 intermediateResult   I
                2      24     2     i   I
                0      27     0  args   [Ljava/lang/String;
    

    内循环案例

    Code:
          stack=2, locals=3, args_size=1
             0: iconst_0
             1: istore_1
             2: iload_1
             3: sipush        1000
             6: if_icmpge     26
             9: iload_1
            10: iconst_2
            11: iadd
            12: istore_2
            13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
            16: iload_2
            17: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
            20: iinc          1, 1
            23: goto          2
            26: return
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
               13       7     2 intermediateResult   I
                2      24     1     i   I
                0      27     0  args   [Ljava/lang/String;
    

    如果您密切注意,只有分配给iintermediateResult 中的LocalVariableTableSlot 被交换为它们出现顺序的产物。 slot 上的相同差异反映在其他代码行中。

    • 没有执行额外的操作
    • intermediateResult 在这两种情况下仍然是一个局部变量,所以访问时间没有区别。

    奖金

    编译器进行了大量优化,看看在这种情况下会发生什么。

    零工作案例

    for(int i=0; i < 1000; i++){
        int intermediateResult = i;
        System.out.println(intermediateResult);
    }
    

    零工作反编译

    for(int i = 0; i < 1000; ++i) {
        System.out.println(i);
    }
    

    【讨论】:

      【解决方案3】:

      在 Go 中尝试了同样的事情,并将使用 go tool compile -S 的编译器输出与 go 1.9.4 进行了比较

      零差异,根据汇编器输出。

      【讨论】:

        【解决方案4】:

        a 还是 b 哪个更好?

        从性能的角度来看,您必须对其进行衡量。 (在我看来,如果你能测量出差异,编译器就不是很好)。

        从维护的角度来看,b 更好。在尽可能窄的范围内,在同一个地方声明和初始化变量。不要在声明和初始化之间留下空隙,也不要污染你不需要的命名空间。

        【讨论】:

        • 而不是Double,如果它处理String,还是“b”的情况更好?
        • @Antoops - 是的, b 更好,原因与声明的变量的数据类型无关。为什么字符串会有所不同?
        【解决方案5】:

        这是更好的形式

        double intermediateResult;
        int i = byte.MinValue;
        
        for(; i < 1000; i++)
        {
        intermediateResult = i;
        System.out.println(intermediateResult);
        }
        

        1) 以这种方式声明一次时间两个变量,而不是每个循环。 2)分配它比所有其他选项更胖。 3) 所以最佳实践规则是迭代之外的任何声明。

        【讨论】:

          【解决方案6】:

          如果有人感兴趣,我用 Node 4.0.0 测试了 JS。在循环外声明导致平均超过 1000 次试验的性能提高约 0.5 毫秒,每次试验有 1 亿次循环迭代。所以我会说继续以最易读/可维护的方式编写它,即 B,imo。我会把我的代码放在一个小提琴中,但我使用了性能-now Node 模块。代码如下:

          var now = require("../node_modules/performance-now")
          
          // declare vars inside loop
          function varInside(){
              for(var i = 0; i < 100000000; i++){
                  var temp = i;
                  var temp2 = i + 1;
                  var temp3 = i + 2;
              }
          }
          
          // declare vars outside loop
          function varOutside(){
              var temp;
              var temp2;
              var temp3;
              for(var i = 0; i < 100000000; i++){
                  temp = i
                  temp2 = i + 1
                  temp3 = i + 2
              }
          }
          
          // for computing average execution times
          var insideAvg = 0;
          var outsideAvg = 0;
          
          // run varInside a million times and average execution times
          for(var i = 0; i < 1000; i++){
              var start = now()
              varInside()
              var end = now()
              insideAvg = (insideAvg + (end-start)) / 2
          }
          
          // run varOutside a million times and average execution times
          for(var i = 0; i < 1000; i++){
              var start = now()
              varOutside()
              var end = now()
              outsideAvg = (outsideAvg + (end-start)) / 2
          }
          
          console.log('declared inside loop', insideAvg)
          console.log('declared outside loop', outsideAvg)
          

          【讨论】:

            【解决方案7】:

            我做了一个简单的测试:

            int b;
            for (int i = 0; i < 10; i++) {
                b = i;
            }
            

            for (int i = 0; i < 10; i++) {
                int b = i;
            }
            

            我用 gcc - 5.2.0 编译了这些代码。然后我反汇编了main() 这两个代码,这就是结果:

            1º:

               0x00000000004004b6 <+0>:     push   rbp
               0x00000000004004b7 <+1>:     mov    rbp,rsp
               0x00000000004004ba <+4>:     mov    DWORD PTR [rbp-0x4],0x0
               0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
               0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
               0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
               0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
               0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
               0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
               0x00000000004004d3 <+29>:    mov    eax,0x0
               0x00000000004004d8 <+34>:    pop    rbp
               0x00000000004004d9 <+35>:    ret
            

               0x00000000004004b6 <+0>: push   rbp
               0x00000000004004b7 <+1>: mov    rbp,rsp
               0x00000000004004ba <+4>: mov    DWORD PTR [rbp-0x4],0x0
               0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
               0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
               0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
               0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
               0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
               0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
               0x00000000004004d3 <+29>:    mov    eax,0x0
               0x00000000004004d8 <+34>:    pop    rbp
               0x00000000004004d9 <+35>:    ret 
            

            这与 asm 结果完全相同。不是证明这两个代码产生相同的东西吗?

            【讨论】:

            • 是的,你这样做很酷,但这又回到了人们所说的语言/编译器依赖性。我想知道 JIT 或解释性语言的性能会受到怎样的影响。
            【解决方案8】:

            嗯,你总是可以为此做一个范围:

            { //Or if(true) if the language doesn't support making scopes like this
                double intermediateResult;
                for (int i=0; i<1000; i++) {
                    intermediateResult = i;
                    System.out.println(intermediateResult);
                }
            }
            

            这样你只声明一次变量,离开循环它就会死掉。

            【讨论】:

              【解决方案9】:

              以下是我在.NET中编写和编译的。

              double r0;
              for (int i = 0; i < 1000; i++) {
                  r0 = i*i;
                  Console.WriteLine(r0);
              }
              
              for (int j = 0; j < 1000; j++) {
                  double r1 = j*j;
                  Console.WriteLine(r1);
              }
              

              CIL 被渲染回代码时,这是我从.NET Reflector 得到的。

              for (int i = 0; i < 0x3e8; i++)
              {
                  double r0 = i * i;
                  Console.WriteLine(r0);
              }
              for (int j = 0; j < 0x3e8; j++)
              {
                  double r1 = j * j;
                  Console.WriteLine(r1);
              }
              

              所以编译后两者看起来完全一样。在托管语言中,代码被转换为 CL/字节代码,并在执行时被转换为机器语言。因此,在机器语言中,甚至可能不会在堆栈上创建双精度数。它可能只是一个寄存器,因为代码反映它是WriteLine 函数的临时变量。对于循环,有一整套优化规则。所以一般人不应该担心它,尤其是在托管语言中。在某些情况下,您可以优化管理代码,例如,如果您必须仅使用 string a; a+=anotherstring[i] 与使用 StringBuilder 连接大量字符串。两者之间的性能差异很大。在很多这样的情况下,编译器无法优化您的代码,因为它无法确定更大范围内的意图。但它几乎可以为您优化基本的东西。

              【讨论】:

              • int j = 0 for (; j
              【解决方案10】:

              这是 VB.NET 中的一个问题。 Visual Basic 结果不会重新初始化此示例中的变量:

              For i as Integer = 1 to 100
                  Dim j as Integer
                  Console.WriteLine(j)
                  j = i
              Next
              
              ' Output: 0 1 2 3 4...
              

              这将在第一次打印 0(Visual Basic 变量在声明时具有默认值!)但之后每次都打印i

              不过,如果您添加 = 0,您会得到预期的结果:

              For i as Integer = 1 to 100
                  Dim j as Integer = 0
                  Console.WriteLine(j)
                  j = i
              Next
              
              'Output: 0 0 0 0 0...
              

              【讨论】:

              • 我已经使用 VB.NET 多年了,从来没有遇到过这个!
              • 是的,在实践中解决这个问题很不愉快。
              • 这是来自 Paul Vick 的参考:panopticoncentral.net/archive/2006/03/28/11552.aspx
              • @eschneider @ferventcoder 不幸的是@PaulV 已经决定drop his old blog posts,所以现在这是一个死链接。
              • 是的,最近遇到了这个;正在寻找有关此的一些官方文档...
              【解决方案11】:

              这取决于语言 - IIRC C# 对此进行了优化,因此没有任何区别,但 JavaScript(例如)每次都会完成整个内存分配。

              【讨论】:

              • 是的,但这并不多。我用 for 循环执行了 1 亿次的简单测试,我发现支持在循环外声明的最大差异是 8 毫秒。它通常更像 3-4 并且偶尔在循环外声明执行更差(最多 4 毫秒),但这并不典型。
              【解决方案12】:

              从性能的角度来看,外面(很多)更好。

              public static void outside() {
                  double intermediateResult;
                  for(int i=0; i < Integer.MAX_VALUE; i++){
                      intermediateResult = i;
                  }
              }
              
              public static void inside() {
                  for(int i=0; i < Integer.MAX_VALUE; i++){
                      double intermediateResult = i;
                  }
              }
              

              我分别执行了这两个函数 10 亿次。 outside() 花费了 65 毫秒。 inside() 耗时 1.5 秒。

              【讨论】:

              • 那一定是Debug未优化编译吧?
              • int j = 0 for (; j
              【解决方案13】:

              这是一个有趣的问题。根据我的经验,当你为代码争论这个问题时,需要考虑一个终极问题:

              有什么理由为什么需要全局变量?

              在全局范围内只声明一次变量而不是在本地声明多次是有意义的,因为这样更有利于组织代码并且需要更少的代码行。但是,如果它只需要在一个方法中本地声明,我会在该方法中对其进行初始化,因此很明显该变量与该方法完全相关。如果您选择后一种选项,请注意不要在初始化它的方法之外调用此变量——您的代码将不知道您在说什么并会报告错误。

              另外,作为旁注,不要在不同方法之间重复局部变量名称,即使它们的用途几乎相同;它只是变得混乱。

              【讨论】:

              • 大声笑我不同意有很多原因......但是,没有反对票......我尊重你的选择权
              【解决方案14】:

              我的做法如下:

              • 如果变量的类型很简单 (int, double, ...) 我更喜欢变体 b(内部)。
                原因: 减少变量的范围。

              • 如果变量类型不简单(某种classstruct我更喜欢变体a(外部)。
                原因: 减少 ctor-dtor 调用次数。

              【讨论】:

                【解决方案15】:

                如果你在 lambda 等变量中使用变量,C# 会有所不同。但一般来说,编译器基本上会做同样的事情,假设变量只在循环中使用。

                鉴于它们基本相同:请注意,版本 b 让读者更清楚地知道该变量不是,也不能在循环之后使用。此外,版本 b 更容易重构。在版本 a 中将循环体提取到自己的方法中更加困难。 而且,版本 b 向您保证这种重构没有副作用。

                因此,版本 a 让我烦恼不已,因为它没有任何好处,而且它使推理代码变得更加困难......

                【讨论】:

                  【解决方案16】:

                  A) 比 B) 更安全............想象一下,如果你在循环中初始化结构而不是 'int' 或 'float' 那么会怎样?

                  喜欢

                  typedef struct loop_example{
                  
                  JXTZ hi; // where JXTZ could be another type...say closed source lib 
                           // you include in Makefile
                  
                  }loop_example_struct;
                  
                  //then....
                  
                  int j = 0; // declare here or face c99 error if in loop - depends on compiler setting
                  
                  for ( ;j++; )
                  {
                     loop_example loop_object; // guess the result in memory heap?
                  }
                  

                  您肯定会遇到内存泄漏问题!因此,我相信“A”是更安全的选择,而“B”容易受到内存积累的影响,尤其是在使用封闭源代码库时。您可以在 Linux 上使用“Valgrind”工具特别是子工具“Helgrind”进行检查。

                  【讨论】:

                    【解决方案17】:

                    我一直认为,如果您在循环中声明变量,那么您就是在浪费内存。如果你有这样的事情:

                    for(;;) {
                      Object o = new Object();
                    }
                    

                    那么不仅需要为每次迭代创建对象,而且需要为每个对象分配一个新的引用。似乎如果垃圾收集器很慢,那么您将有一堆需要清理的悬空引用。

                    但是,如果你有这个:

                    Object o;
                    for(;;) {
                      o = new Object();
                    }
                    

                    那么您每次只创建一个引用并为其分配一个新对象。当然,它可能需要更长的时间才能超出范围,但是只有一个悬空引用需要处理。

                    【讨论】:

                    • 不会为每个对象分配新的引用,即使引用是在“for”循环中声明的。在这两种情况下: 1) 'o' 是一个局部变量,堆栈空间在函数开始时为其分配一次。 2)每次迭代都会创建一个新对象。所以性能上没有区别。对于代码组织、可读性和可维护性,在循环内声明引用会更好。
                    • 虽然我不能代表 Java,但在 .NET 中,第一个示例中的每个对象都没有“分配”引用。该本地(方法)变量的堆栈上有一个条目。对于您的示例,创建的 IL 是相同的。
                    【解决方案18】:

                    一位同事更喜欢第一种形式,告诉它​​是一种优化,更喜欢重复使用声明。

                    我更喜欢第二个(并尝试说服我的同事!;-)),读过:

                    • 它将变量的范围缩小到需要它们的位置,这是一件好事。
                    • Java 优化到足以使性能没有显着差异。 IIRC,也许第二种形式更快。

                    无论如何,它属于依赖编译器和/或 JVM 质量的过早优化类别。

                    【讨论】:

                      【解决方案19】:

                      在我看来,b 是更好的结构。在 a 中,intermediateResult 的最后一个值会在循环结束后保留​​。

                      编辑: 这与值类型没有太大区别,但引用类型可能有点重。就我个人而言,我喜欢尽快取消引用变量以进行清理,而 b 会为您做到这一点,

                      【讨论】:

                      • sticks around after your loop is finished - 虽然这在 Python 这样的语言中无关紧要,在函数结束之前绑定名称一直存在。
                      • @new123456:OP 要求提供 Java 细节,即使问题 询问得有些笼统。许多 C 派生语言都有块级作用域:C、C++、Perl(带有 my 关键字)、C# 和 Java 等等我用过的 5 个。
                      • 我知道 - 这是一个观察,而不是批评。
                      【解决方案20】:

                      好吧,我分别运行了 A 和 B 示例 20 次,循环了 1 亿次。(JVM - 1.5.0)

                      A:平均执行时间:0.074 秒

                      B:平均执行时间:0.067 秒

                      令我惊讶的是,B 稍微快了一点。 与计算机一样快,现在很难说你是否能准确地测量到这一点。 我也会将其编码为 A 方式,但我会说这并不重要。

                      【讨论】:

                      • 你打败了我我正要发布我的分析结果,我得到了或多或少相同的结果,是的,令人惊讶的是,B 更快,如果我需要下注,真的会想到 A。
                      • 不足为奇——当变量是循环的局部变量时,它不需要在每次迭代后保存,所以它可以留在寄存器中。
                      • +1 表示 实际测试,而不仅仅是 OP 自己编造的观点/理论。
                      • @GoodPerson 老实说,我希望这样做。我在我的机器上运行了这个测试大约 10 次,迭代了 50,000,000-100,000,000 次,使用几乎相同的一段代码(我很乐意与任何想要运行统计数据的人分享)。答案几乎均分,通常相差 900 毫秒(超过 5000 万次迭代),这并不算多。虽然我的第一个想法是它会是“噪音”,但它可能会稍微倾斜一点。不过,这项努力对我来说似乎纯粹是学术性的(对于大多数现实生活中的应用程序)。无论如何我都希望看到结果;)有人同意吗?
                      • 在不记录设置的情况下显示测试结果是毫无价值的。在这种情况下尤其如此,因为两个代码片段都产生相同的字节码,所以任何测量的差异都只是测试条件不足的标志。
                      【解决方案21】:

                      即使我知道我的编译器足够聪明,我也不喜欢依赖它,并且会使用 a) 变体。

                      只有当你迫切需要在循环体之后使 intermediateResult 不可用时,b) 变体才对我有意义。但无论如何,我无法想象这种绝望的情况......

                      编辑:Jon Skeet 提出了一个很好的观点,表明循环内的变量声明可以产生实际的语义差异。

                      【讨论】:

                        【解决方案22】:

                        我会一直使用 A(而不是依赖编译器)并且也可能重写为:

                        for(int i=0, double intermediateResult=0; i<1000; i++){
                            intermediateResult = i;
                            System.out.println(intermediateResult);
                        }
                        

                        这仍将intermediateResult 限制在循环的范围内,但不会在每次迭代期间重新声明。

                        【讨论】:

                        • 您在概念上是否希望变量在循环期间存在,而不是在每次迭代中单独存在?我很少这样做。编写尽可能清楚地表明您的意图的代码,除非您有非常非常好的理由不这样做。
                        • 啊,不错的妥协,我从来没有想过这个! IMO,代码确实变得不那么“清晰”了)
                        • @Jon - 我不知道 OP 实际上对中间值做了什么。只是觉得这是一个值得考虑的选择。
                        【解决方案23】:

                        这取决于语言和具体用途。例如,在 C# 1 中它没有任何区别。在 C# 2 中,如果局部变量由匿名方法(或 C# 3 中的 lambda 表达式)捕获,则可以产生非常显着的差异。

                        例子:

                        using System;
                        using System.Collections.Generic;
                        
                        class Test
                        {
                            static void Main()
                            {
                                List<Action> actions = new List<Action>();
                        
                                int outer;
                                for (int i=0; i < 10; i++)
                                {
                                    outer = i;
                                    int inner = i;
                                    actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer));
                                }
                        
                                foreach (Action action in actions)
                                {
                                    action();
                                }
                            }
                        }
                        

                        输出:

                        Inner=0, Outer=9
                        Inner=1, Outer=9
                        Inner=2, Outer=9
                        Inner=3, Outer=9
                        Inner=4, Outer=9
                        Inner=5, Outer=9
                        Inner=6, Outer=9
                        Inner=7, Outer=9
                        Inner=8, Outer=9
                        Inner=9, Outer=9
                        

                        不同之处在于所有操作都捕获相同的outer 变量,但每个操作都有自己独立的inner 变量。

                        【讨论】:

                        • 在示例 B(原始问题)中,它实际上每次都会创建一个新变量吗?堆栈中发生了什么?
                        • @Jon,这是 C# 1.0 中的错误吗?理想情况下 Outer 不应该是 9 吗?
                        • @nawfal:我不明白你的意思。 Lambda 表达式不在 1.0 中...而 Outer is 9。你的意思是什么错误?
                        • @nawfal:我的意思是,C# 1.0 中没有任何语言功能可以让您区分在循环内声明变量和在外部声明变量(假设两者都已编译)。这在 C# 2.0 中发生了变化。没有错误。
                        • @JonSkeet 哦,是的,我现在明白了,我完全忽略了这样一个事实,即你不能在 1.0 中关闭像这样的变量,我的错! :)
                        【解决方案24】:

                        作为一般规则,我在最内层可能的范围内声明我的变量。所以,如果你没有在循环之外使用intermediateResult,那么我会选择B。

                        【讨论】:

                          【解决方案25】:

                          我怀疑一些编译器可以将两者优化为相同的代码,但肯定不是全部。所以我会说你最好选择前者。后者的唯一原因是,如果您想确保在循环中使用声明的变量。

                          【讨论】:

                            【解决方案26】:

                            我认为这取决于编译器,很难给出一般性答案。

                            【讨论】:

                              猜你喜欢
                              • 1970-01-01
                              • 1970-01-01
                              • 2010-09-06
                              • 1970-01-01
                              • 1970-01-01
                              • 2015-12-09
                              • 2011-04-07
                              • 1970-01-01
                              • 2010-09-19
                              相关资源
                              最近更新 更多