关于java:在循环之前或循环中声明变量之间的区别?

Difference between declaring variables before or in loop?

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

(a)循环前声明:

ZZU1

(b)循环内的语句(重复):

ZZU1

哪个比较好,A还是B?

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

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


哪个更好,A还是B?

从性能的角度来看,你必须衡量它。(在我看来,如果你能测量一个差异,编译器就不是很好了)。

从维护的角度来看,B更好。在尽可能窄的范围内,在同一位置声明和初始化变量。不要在声明和初始化之间留下空白,也不要污染您不需要的名称空间。


我把你的A和B示例分别运行了20次,循环了1亿次。(jvm-1.5.0)

A:平均执行时间:074秒

B:平均执行时间:.067秒

令我惊讶的是B的速度有点快。像计算机一样快,现在很难说你能不能精确地测量这个。我也会用A的方式来编码,但我会说这并不重要。


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

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();
        }
    }
}

输出:

1
2
3
4
5
6
7
8
9
10
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变量。


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

1
2
3
4
5
6
7
8
9
10
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);
}

这是我从.NET Reflector得到的,当CIL被呈现回代码时。

1
2
3
4
5
6
7
8
9
10
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/字节代码,在执行时转换为机器语言。因此,在机器语言中,甚至不能在堆栈上创建double。它可能只是一个寄存器,因为代码反映它是WriteLine函数的临时变量。对于循环有一整套优化规则。所以一般人不应该担心它,特别是在管理语言中。有些情况下,您可以优化管理代码,例如,如果您必须仅使用string a; a+=anotherstring[i]与使用StringBuilder连接大量字符串。两者的性能差别很大。在很多这样的情况下,编译器无法优化代码,因为它无法确定在更大的范围内要做什么。但是它可以为你优化基本的东西。


这是vb.net中的一个gotcha。Visual Basic结果不会重新初始化此示例中的变量:

1
2
3
4
5
6
7
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变量具有默认值!)但在那之后,每次都是以东十一〔三〕号。

但是,如果您添加一个= 0,您将得到您可能期望的结果:

1
2
3
4
5
6
7
For i as Integer = 1 to 100
    Dim j as Integer = 0
    Console.WriteLine(j)
    j = i
Next

'Output: 0 0 0 0 0...


我做了一个简单的测试:

1
2
3
4
int b;
for (int i = 0; i < 10; i++) {
    b = i;
}

VS

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

我用gcc-5.2.0编译了这些代码。然后我拆卸了主机这两个代码的结果是:

1O:

1
2
3
4
5
6
7
8
9
10
11
12
   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

VS

2O

1
2
3
4
5
6
7
8
9
10
11
12
   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结果完全相同。这两个代码产生的结果是一样的,这不是一个证据吗?


它依赖于语言——IIRC C对此进行了优化,因此没有任何区别,但JavaScript(例如)每次都会进行整个内存分配。


我总是使用(而不是依赖于编译器),也可能重写为:

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

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


在我看来,B是更好的结构。在A中,IntermediateResult的最后一个值在循环结束后保持不变。

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


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


作为一般规则,我在尽可能内部的范围内声明变量。所以,如果您不在循环之外使用intermediateresult,那么我将使用b。


同事更喜欢第一个表单,告诉它是一个优化,更喜欢重用声明。

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

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

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


好吧,你总是可以为这个做一个范围:

1
2
3
4
5
6
7
{ //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);
    }
}

这样,您只声明一次变量,当您离开循环时,它将死亡。


如果您在lambda等中使用变量,则C中存在差异。但通常情况下,编译器将执行相同的操作,假设变量仅在循环中使用。

考虑到它们基本上是相同的:请注意,版本B使读者更明显地认识到变量在循环之后不是也不能使用。此外,版本B更容易重构。在版本A中,将循环体提取到它自己的方法中更为困难。此外,版本B向您保证,这样的重构没有副作用。

因此,版本A让我非常恼火,因为它没有任何好处,而且它使得对代码的解释变得更加困难…


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

1
2
3
for(;;) {
  Object o = new Object();
}

然后,不仅需要为每个迭代创建对象,还需要为每个对象分配一个新的引用。如果垃圾收集器运行缓慢,那么您将有大量悬空的引用需要清理。

但是,如果您有:

1
2
3
4
Object o;
for(;;) {
  o = new Object();
}

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


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


我的做法是:

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

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


从性能的角度来看,外部更好。

1
2
3
4
5
6
7
8
9
10
11
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毫秒。内侧()用了1.5秒。


我有这个问题很久了。所以我测试了一段更简单的代码。

结论:在这种情况下,没有表现差异。

外回路箱

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

内环箱

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

我检查了intellij的反编译器上编译的文件,对于这两种情况,我得到了相同的Test.class

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

我还使用这个答案中给出的方法分解了这两个案例的代码。我只显示与答案相关的部分

外回路箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

内环箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

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

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

奖金

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

零工作案例

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

零功反编译

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


a)是安全的赌注而不是b)……想象一下,如果您是在循环中初始化结构,而不是"int"或"float",那么是什么?

喜欢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"进行检查。


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

变量需要是全局变量有什么原因吗?

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

另外,作为补充说明,即使不同方法的目的几乎相同,也不要在它们之间复制局部变量名;这会让人困惑。


如果有人感兴趣,我用node 4.0.0测试了JS。在循环外声明导致了大约5毫秒的性能改进,平均超过1000个测试,每个测试有1亿个循环迭代。所以我要说"继续",用最可读/可维护的方式来写它,即b,imo。我会把我的代码放在一把小提琴上,但我使用了现在的性能节点模块。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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)

这是比较好的形式

1
2
3
4
5
6
7
8
double intermediateResult;
int i = byte.MinValue;

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

1)以这种方式声明一次时间都是变量,而不是每个都是循环。2)任务是所有其他选项中最重要的。3)所以最佳实践规则是迭代之外的任何声明。


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

零差,根据汇编程序输出。


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

只有当你迫切需要在循环体之后使中间结果不可用时,b)变量才对我有意义。但无论如何,我无法想象如此绝望的局面……

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