在循环之前声明变量和在循环中声明变量的区别是什么?

345

我一直想知道,在一般情况下,将一个临时变量在循环之前声明,相对于在循环内部重复声明,是否会有任何(性能)差异?以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代码时非常重要。Google建议对于时间关键的代码,将递增变量声明在for循环外部,就像在for循环内部一样,因为在那个环境中每次都会重新声明它。对于昂贵的算法,性能差异非常明显。 - aaroncarsonart
1
@AaronCarson,您能否提供谷歌此建议的链接? - Vitaly Zinchenko
【相关问题】C++中的同一个问题:c++ - 在循环内声明变量是好习惯还是坏习惯?- Stack Overflow - user202729
26个回答

5

我猜测一些编译器可以将两者优化为相同的代码,但肯定不是所有编译器都能这样做。所以我认为你最好选择前者。后者唯一的原因是如果你想确保在循环中仅使用已声明的变量


5

通常情况下,我会在最内层的范围中声明变量。因此,如果您在循环之外不使用intermediateResult,则应选择B。


5

一位同事更喜欢第一种形式,他说这是一种优化,更喜欢重复使用声明。

我更喜欢第二种形式(并试图说服我的同事!;-)),因为我读到了以下内容:

  • 它将变量的范围减少到需要它们的地方,这是一个好事情。
  • Java已经进行了足够的优化,不会对性能产生重大影响。如果我没记错的话,第二种形式甚至更快。

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


5

如果您在lambda等中使用变量,则C#中会有所不同。 但是一般情况下,编译器假定变量仅在循环内部使用时,基本上会执行相同的操作。

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

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


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

4

我一直认为如果你在循环内部声明变量,那么你会浪费内存。如果你有像这样的代码:

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

每次迭代都需要创建对象,并且需要为每个对象分配一个新的引用。如果垃圾回收器速度较慢,则可能会有一堆需要清理的悬空引用。

但是,如果您有以下代码:

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

那么你只是创建了一个单一的引用,并且每次都将一个新对象分配给它。当然,它可能需要更长的时间才能超出范围,但是这样只有一个悬空引用需要处理。


3
即使在“for”循环内声明引用,也不会为每个对象分配新的引用。在两种情况下: 1)“o”是本地变量,在函数开始时分配一次堆栈空间。 2)在每次迭代中都创建一个新的对象。因此,在性能上没有区别。为了代码组织、可读性和可维护性,在循环内声明引用更好。 - Ajoy Bhatia
1
虽然我不能代表Java说话,但在.NET中,第一个示例中每个对象的引用并没有被“分配”。对于该本地(方法内)变量,堆栈上只有一个条目。对于您的示例,创建的IL是相同的。 - Jesse C. Slicer

3

我的做法如下:

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

  • 如果变量的类型不是简单的 (某种类或结构体),我更喜欢使用变量 a(外部)。
    原因: 减少构造函数和析构函数的调用次数。


3

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

结论:对于这种情况,没有性能差异。

循环外部的情况

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

我也按照这个答案中的方法,对这两种情况的代码进行了反汇编。我只展示与答案相关的部分。 循环外部情况
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;

如果你仔细观察,只有在LocalVariableTable中分配给iintermediateResultSlot根据它们出现的顺序进行了交换。同样的差异在其他代码行中也有体现。
  • 没有执行额外的操作
  • 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);
}

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


2
一定是未优化的调试编译,对吧? - Tomasz Przychodzki
int j = 0,这样声明一次变量,而不是每个循环都声明。2)赋值比其他选项更快。3)因此,最佳实践规则是在迭代for之外进行任何声明。 - luka

1

如果有人感兴趣,我已经用Node 4.0.0测试了JS。在循环外声明平均可以提高每个试验1000次,每次100 million循环迭代的性能约为0.5毫秒。因此,在我看来,最好以最可读/可维护的方式编写代码。我会把代码放在fiddle里面,但我使用了performance-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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接