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

时间:2009-01-02 16:06:40

标签: java performance loops variables initialization

我一直想知道,一般来说,在循环之前声明一个抛弃变量,而不是反复在循环内,是否会产生任何(性能)​​差异? 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);
}

哪一个更好,一个 b

我怀疑重复的变量声明(例如 b )会在理论上产生更多的开销 ,但编译器足够智能,因此无关紧要。示例 b 具有更紧凑的优点,并将变量的范围限制为使用它的位置。不过,我倾向于根据示例编写 a

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

26 个答案:

答案 0 :(得分:245)

哪个更好, a b

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

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

答案 1 :(得分:208)

我运行你的A和B示例各20次,循环1亿次。(JVM - 1.5.0)

A:平均执行时间:.074秒

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

令我惊讶的是B稍快一些。 如果能够准确地测量计算机,现在很难说计算机。 我会把它编码为A方式,但我会说它并不重要。

答案 2 :(得分:66)

这取决于语言和确切用途。例如,在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变量。

答案 3 :(得分:35)

以下是我在.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);
}

这是.NET ReflectorCIL呈现回代码时的结果。

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来连接大量字符串。两者之间的表现有很大差异。有很多这样的情况,编译器无法优化您的代码,因为它无法弄清楚更大范围内的目标。但它可以为你优化基本的东西。

答案 4 :(得分:24)

这是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...

答案 5 :(得分:15)

我做了一个简单的测试:

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

vs

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

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

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

VS

   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 

其中exaclty与结果相同。是不是两个代码产生相同的东西的证明?

答案 6 :(得分:11)

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

答案 7 :(得分:11)

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

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

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

答案 8 :(得分:6)

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

编辑: 这与值类型没有太大区别,但引用类型可能有些重要。就个人而言,我喜欢尽快解除引用的变量以进行清理,b为你做这件事,

答案 9 :(得分:5)

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

答案 10 :(得分:5)

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

答案 11 :(得分:5)

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

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

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

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

答案 12 :(得分:5)

如果你在lambda等中使用变量,那么C#就有区别。但是通常编译器基本上会做同样的事情,假设变量只在循环中使用。

鉴于它们基本相同:请注意,版本b使得读者更加明显地认为变量不是,也不能在循环之后使用。此外,版本b更容易重构。在版本a中将循环体提取到自己的方法中更加困难。此外,版本b确保对这种重构没有副作用。

因此,版本a让我感到厌烦,因为它没有任何好处,它使代码更难以推理......

答案 13 :(得分:5)

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

{ //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);
    }
}

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

答案 14 :(得分:4)

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

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

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

但是,如果你有这个:

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

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

答案 15 :(得分:3)

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

答案 16 :(得分:3)

我的做法是:

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

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

答案 17 :(得分:1)

从表现的角度来看,外面的(好)更好。

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秒。

答案 18 :(得分:0)

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'。

答案 19 :(得分:0)

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

这个变量需要是全局的吗?

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

另外,作为附注,不要在不同方法之间复制局部变量名,即使它们的目的几乎相同;它只是让人困惑。

答案 20 :(得分:0)

如果有兴趣的话,我用Node 4.0.0测试了JS。在循环外声明导致平均超过1000次试验的性能提高约0.5ms,每次试验有1亿次循环迭代。所以我要说继续以最可读/可维护的方式编写它,即B,imo。我会把我的代码放在一个小提琴中,但我使用了性能现在的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)

答案 21 :(得分:0)

这是更好的形式

double intermediateResult;
int i = byte.MinValue;

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

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

答案 22 :(得分:0)

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

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

答案 23 :(得分:0)

很长一段时间我都遇到过同样的问题。所以我测试了一个更简单的代码。

结论:对于此类情况没有性能差异。

外循环案例

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的反编译器上检查了已编译的文件,在两种情况下,我都得到了相同 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;

如果您密切注意,则仅将Slot中分配给iintermediateResult的{​​{1}}交换为它们出现顺序的乘积。相同的插槽差异反映在其他代码行中。

  • 没有执行额外的操作
  • LocalVariableTable在两种情况下仍是局部变量,因此访问时间没有差异。

奖励

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

零工作案例

intermediateResult

零工作已反编译

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

答案 24 :(得分:0)

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

答案 25 :(得分:-1)

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

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

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