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


312

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

ab哪个更好

我怀疑重复变量声明(示例b在理论上会增加开销,但编译器足够聪明,因此无关紧要。示例b的优点是更紧凑,并将变量的范围限制在使用它的位置。尽管如此,我还是倾向于根据示例a进行编码。

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


在为Android平台编写Java代码时,这一点很重要。Google建议对于时间紧迫的代码在for循环之外声明增量变量,就像在for循环内部一样,它每次都在该环境中重新声明它。对于昂贵的算法,性能差异非常明显。
亚伦·卡森(AaronCarson)2015年

1
@AaronCarson请你通过谷歌提供连结此建议
维塔利·津琴科

Answers:


256

ab哪个更好?

从性能角度来看,您必须对其进行评估。(我认为,如果您可以衡量差异,则编译器不是很好)。

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


5
如果它处理String,而不是Double,还是更好的情况是“ b”吗?
Antoops 2014年

3
@Antoops-是的,由于与声明的变量的数据类型无关的原因,b更好。为什么字符串会有所不同?
Daniel Earwicker 2015年

215

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

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

B:平均执行时间:0.067秒

令我惊讶的是,B的速度稍快一些。现在很难说出与计算机一样快的精度。我也会用A方式进行编码,但我说这并不重要。


12
你打败了我,我正要发布我的结果进行概要分析,我或多或少都得到了相同的结果,是的,令人惊讶的是,如果我需要下注B,那么B确实会更快地想到A。
Mark Davidson

14
不足为奇-当变量在循环本地时,不需要在每次迭代后保留它,因此它可以保留在寄存器中。

142
+1用于实际测试,而不仅仅是OP可以自己提出的意见/理论。
MGOwen

3
@GoodPerson,老实说,我希望做到这一点。我使用几乎相同的代码在我的计算机上运行了此测试10次,进行了50,000,000至100,000,000次迭代(我希望与希望运行统计信息的任何人共享该代码)。答案通常以900ms的余量(超过50M的迭代)几乎均等地分配了这两种方法,实际上并不是很多。尽管我首先想到的是它将成为“噪音”,但它可能会稍微倾斜一点。不过,对于我来说,这种努力似乎纯粹是学术上的(对于大多数现实生活中的应用而言)。我还是希望看到结果;)有人同意吗?
javatarz

3
在没有记录设置的情况下显示测试结果是毫无价值的。在这种情况下尤其如此,因为两个代码片段都产生相同的字节码,所以任何测得的差异只是测试条件不足的标志。
Holger

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
在示例B(原始问题)中,它是否每次实际上都会创建一个新变量?堆栈中发生了什么?
罗伊·纳米尔

@Jon,这是C#1.0中的错误吗?理想情况下不应该Outer是9吗?
nawfal 2014年

@nawfal:我不知道你的意思。Lambda表达式不在1.0中,而外部则为 9。您是什么意思?
乔恩·斯基特

@nawfal:我的意思是,C#1.0中没有任何语言功能,您可以分辨出在循环内声明变量与在循环内声明变量之间的区别(假设两者均已编译)。这在C#2.0中发生了变化。没有错误。
乔恩·斯基特

@JonSkeet哦,是的,我现在明白了,我完全忽略了您不能关闭1.0中的变量的事实,我不好!:)
nawfal 2014年

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

这是当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 /字节代码,并且在执行时将其转换为机器语言。因此,在机器语言中,甚至可能不会在堆栈上创建一个double。它可能只是一个寄存器,因为代码反映它是WriteLine函数的临时变量。有整套针对循环的优化规则。因此,普通人不必为此担心,尤其是在托管语言中。在某些情况下,您可以优化管理代码,例如,如果您必须使用just string a; a+=anotherstring[i]与using 来连接大量字符串,StringBuilder。两者之间的性能差异很大。在很多情况下,编译器无法优化您的代码,因为它无法找出更大范围内的目标。但是它可以为您优化基本的东西。


int j = 0 for(; j <0x3e8; j ++)以这种方式一次声明两个变量,而不是每个for周期。2)分配所有其他选项。3)因此,最佳实践规则是迭代之外的任何声明。
luka

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

1
我已经使用VB.NET多年了,还没有遇到这个问题!
ChrisA

12
是的,在实践中弄清楚这一点是不愉快的。
Michael Haren

下面是从保罗维克关于这个参考:panopticoncentral.net/archive/2006/03/28/11552.aspx
ferventcoder

1
@eschneider @ferventcoder不幸的是,@ PaulV已决定删除他的旧博客文章,因此现在这是一个无效的链接。
马克·赫德

是的,最近才遇到这个问题;正在为此寻找一些官方文档...
Eric Sc​​hneider

15

我做了一个简单的测试:

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

2度

   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 

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


3
是的,这样做很酷,但这又回到了人们对语言/编译器依赖性的看法。我不知道JIT或解释语言的性能会受到怎样的影响。
user137717 '16

12

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


是的,但这并不多。我用一个执行了1亿次的for循环运行了一个简单的测试,发现发现在循环外声明的最大区别是8 ms。通常更像3-4,偶尔在循环外声明执行WORSE(最长4 ms),但这不是典型的。
user137717 '16

11

我将始终使用A(而不是依赖于编译器),并且可能还会重写为:

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

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


12
从概念上讲,您是否希望变量在循环持续时间内存在,而不是在每次迭代时都独立存在?我很少。编写代码,尽可能清楚地表明您的意图,除非您有非常非常好的理由这样做。
乔恩·斯基特

4
啊,不错的妥协,我从没想过!IMO,代码的确在视觉上变得“清晰”了)
Rabarberski

2
@Jon-我不知道OP实际使用中间值做什么。只是认为这是一个值得考虑的选择。
三联画

6

我认为b是更好的结构。在a中,循环完成后,intermediateResult的最后一个值仍然存在。

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


sticks around after your loop is finished-尽管在Python这样的语言中这并不重要,但绑定名称会一直存在,直到函数结束。
new123456

@ new123456:OP询问了Java的详细信息,即使这个问题某种程度上是通用的。许多C派生的语言都有块级作用域:C,C ++,Perl(带有my关键字),C#和Java(我已经使用了5个名字)。
Powerlord 2011年

我知道-这只是观察,而不是批评。
new12

5

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


5

通常,我在最内部的范围内声明变量。因此,如果您不在循环之外使用middleResult,那么我会选择B。


5

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

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

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

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


5

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

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

因此,版本a无休止地困扰着我,因为它没有任何好处,并且使推理代码变得更加困难...


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

这样,您只需声明一次变量,并且在退出循环时该变量将死亡。


4

我一直以为,如果在循环内声明变量,那将浪费内存。如果您有这样的事情:

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

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

但是,如果您有:

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

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


3
即使在“ for”循环中声明了引用,也不会为每个对象分配新的引用。在两种情况下:1)'o'是一个局部变量,并且在函数开始时为其分配一次堆栈空间。2)在每次迭代中创建一个新的对象。因此,性能没有差异。对于代码组织,可读性和可维护性,在循环内声明引用更好。
Ajoy Bhatia 2010年

1
尽管我不能说Java,但在第一个示例中,.NET中的引用没有为每个对象“分配”。在堆栈上,该(方法)局部变量只有一个条目。对于您的示例,创建的IL是相同的。
Jesse C. Slicer 2010年


3

我的做法是:

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

  • 如果变量的类型不简单(某种classstruct),则我希望使用变体a(外部)。
    原因:减少了ctor-dtor调用次数。


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
那一定是Debug未优化的编译吧?
Tomasz Przychodzki '16

int j = 0 for(; j <0x3e8; j ++)以这种方式一次声明两个变量,而不是每个声明一次。2)分配所有其他选项。3)因此,最佳实践规则是迭代之外的任何声明。
luka

1

如果有人感兴趣,我使用Node 4.0.0测试了JS。在循环外声明,在1000次试验中平均平均〜.5 ms的性能改进,每个试验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)

0

与B)相比,A)是一个安全的选择......想象一下,如果您是在循环中初始化结构而不是“ 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”则更容易受到内存累积的影响,尤其是在源代码库附近工作。


0

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

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

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

另外,请注意,即使用途不同,也不要在不同方法之间重复使用局部变量名称。只是令人困惑。


1
大声笑我出于多种原因不同意...但是,没有拒绝投票...我尊重您的选择权
2015年

0

这是更好的形式

double intermediateResult;
int i = byte.MinValue;

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

1)用这种方式一次声明两个变量,而不是每个声明一次。2)分配所有其他选项。3)因此,最佳实践规则是迭代之外的任何声明。


0

在Go中进行了相同的尝试,并将使用go tool compile -Sgo 1.9.4 的编译器输出进行了比较

零差,根据汇编器输出。


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

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

外循环情况

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分配到iintermediateResultLocalVariableTable被换为他们的出场顺序的产物。插槽中的相同差异反映在其他代码行中。

  • 没有执行额外的操作
  • 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

即使我知道我的编译器足够聪明,我也不想依靠它,而是会使用a)变体。

仅当您迫切需要在循环正文之后使中间体结果不可用时,b)变体对我才有意义。但无论如何,我无法想象如此绝望的情况。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.