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

Rab*_*ski 307 java variables performance loops initialization

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

a)循环前的声明:

double intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}
Run Code Online (Sandbox Code Playgroud)

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

for(int i=0; i < 1000; i++){
    double intermediateResult = i;
    System.out.println(intermediateResult);
}
Run Code Online (Sandbox Code Playgroud)

哪一个更好,一个还是b

我怀疑重复变量声明(例子b)在理论上会产生更多的开销,但是编译器足够聪明,所以它并不重要.实施例b具有更紧凑的优点,并且将变量的范围限制在其使用的位置.尽管如此,我倾向于根据示例来编码一个.

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

Dan*_*ker 250

哪个更好,a还是b

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

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

  • 而不是Double,如果它处理String,情况"b"仍然更好? (5认同)
  • @Antoops - 是的,b更好的原因与声明的变量的数据类型无关.为什么弦乐会有所不同? (3认同)

Mar*_*ark 211

好吧,我每次运行你的A和B示例20次,循环1亿次.(JVM - 1.5.0)

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

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

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

  • 实际上测试它**的+1,而不仅仅是OP可以自己构成的意见/理论. (138认同)
  • 没什么好吃的 - 当变量是循环的局部变量时,它不需要在每次迭代后保留,因此它可以保留在寄存器中. (14认同)
  • 你打败了我,我正准备发布我的结果进行剖析,我或多或少都相同而且令人惊讶的是如果我需要打赌,B会更快地想到A. (11认同)
  • @GoodPerson老实说,我希望这样做.我在我的机器上运行这个测试大约10次,进行50,000,000-100,000次迭代,几乎完全相同的代码(我希望与任何想要运行统计数据的人分享).答案几乎同样分为两种方式,通常是900毫秒(超过50万次迭代),这并不是很多.虽然我的第一个想法是它会变成"噪音",但它可能会稍微倾斜一点.对于我来说,这种努力看起来纯粹是学术性的(对于大多数现实生活中的应用程序而言)..无论如何我都希望看到结果;)任何人都同意吗? (3认同)
  • JIT开始后的执行时间是多少? (2认同)
  • 打赌我最好的英镑99%投票没有检查. (2认同)
  • 在没有记录设置的情况下显示测试结果是没有价值的.在这种情况下尤其如此,两个代码片段都产生相同的字节码,因此任何测量的差异只是测试条件不足的标志. (2认同)

Jon*_*eet 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

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

  • 在示例B(原始问题)中,它实际上每次都创建一个新变量吗?堆栈眼中发生了什么? (3认同)

par*_*cle 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);
}
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

因此编译后两者看起来完全相同.在托管语言中,代码转换为CL /字节代码,并在执行时将其转换为机器语言.因此,在机器语言中,甚至可能无法在堆栈上创建double.它可能只是一个寄存器,因为代码反映它是WriteLine函数的临时变量.循环有一整套优化规则.所以一般人不应该担心它,特别是在托管语言中.在某些情况下,您可以优化管理代码,例如,如果您必须使用仅string a; a+=anotherstring[i]使用vs 来连接大量字符串StringBuilder.两者之间的表现有很大差异.有很多这样的情况,编译器无法优化您的代码,因为它无法弄清楚更大范围内的目标.但它可以为您优化基本的东西.


Mic*_*ren 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...
Run Code Online (Sandbox Code Playgroud)

这将在第一次打印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...
Run Code Online (Sandbox Code Playgroud)

  • 是的,在实践中弄清楚这一点是不愉快的. (12认同)

小智 15

我做了一个简单的测试:

int b;
for (int i = 0; i < 10; i++) {
    b = i;
}
Run Code Online (Sandbox Code Playgroud)

VS

for (int i = 0; i < 10; i++) {
    int b = i;
}
Run Code Online (Sandbox Code Playgroud)

我用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
Run Code Online (Sandbox Code Playgroud)

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 
Run Code Online (Sandbox Code Playgroud)

哪个是exaclty相同的结果.是不是两个代码产生相同的东西的证据?

  • 是的,你这样做很酷,但这又回到了人们对语言/编译器依赖的看法.我想知道JIT或解释语言表现会受到什么影响. (3认同)

ann*_*ata 12

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


Tri*_*ych 11

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

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

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

  • 您是否在概念上希望变量在循环期间存在而不是每次迭代单独存在?我很少这样做.编写代码,尽可能清楚地显示您的意图,除非您有非常非常好的理由不这样做. (12认同)
  • 啊,妥协妥协,我从没想过这个!IMO,虽然代码确实变得有点不那么明显了 (4认同)
  • @Jon - 我不知道OP实际上对中间值做了什么.只是觉得这是一个值得考虑的选择. (2认同)

Pow*_*ord 6

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

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


Ste*_*w S 5

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


Chr*_*ris 5

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


Phi*_*Lho 5

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

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

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

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


小智 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Mar*_*wul 5

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

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

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