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

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个回答

273

哪个比较好, a 还是 b

从性能的角度来看,你需要进行测量。(而且在我看来,如果你可以测量差异,那么编译器并不是很好)。

从维护的角度来看,b 更好。在尽可能狭窄的作用域内声明和初始化变量。不要在声明和初始化之间留下巨大的空隙,也不要污染不必要的命名空间。


5
如果处理的不是双倍数,而是字符串,那么情况“b”会更好吗? - Anto Varghese
5
@Antoops - 是的,选择 b 更好,并不是因为所声明变量的数据类型。那么对于字符串来说,为什么会有不同呢? - Daniel Earwicker

235

我运行了你的A和B示例各20次,在1.5.0版本的JVM上循环1亿次。

A: 平均执行时间为:0.074秒

B: 平均执行时间为:0.067秒

令人惊讶的是,B略微快一些。由于现在计算机的速度非常快,很难说你是否能够准确地测量这一点。我也会像A那样编码,但我认为这并不重要。


13
你打败我了,我正准备发布我的分析结果,我得到的结果和你差不多,而且令人惊讶的是B更快,如果我必须打赌的话,我本来以为A会更快。 - Mark Davidson
17
当变量在循环内部是局部变量时,它不需要在每次迭代后被保存,因此可以留在寄存器中,这并不令人感到惊讶。 - user3458
148
+1 是为了实际测试,而不仅仅是楼主自己可能想出来的观点/理论。 - MGOwen
3
老实说,我希望能够做到这一点。我在我的计算机上使用几乎相同的代码运行了这个测试大约10次,迭代次数为5000万到1亿次之间。答案基本上是两种可能性,通常相差不到900毫秒(在5000万次迭代中),这并不算很多。虽然我最初的想法是这只是“噪音”,但它可能稍微倾向于其中一种结果。尽管我认为这项工作纯粹是学术性的(对于大多数实际应用程序来说)...无论如何,我都很想看到结果 ;)有人同意吗? - javatarz
5
如果没有记录测试设置,仅展示测试结果是毫无意义的。特别是在这种情况下,两个代码片段生成相同的字节码,因此任何测量差异只是测试条件不充分的迹象。 - Holger
显示剩余9条评论

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中(原问题),它实际上每次都创建了一个新变量吗?在堆栈的视角下会发生什么? - Royi Namir
@Jon,这是C# 1.0的一个bug吗?理论上Outer不应该是9吗? - nawfal
@nawfal:我不知道你的意思。Lambda表达式不在1.0中...而且Outer 9。你指的是哪个错误? - Jon Skeet
@nawfal:我的观点是,在C# 1.0中没有任何语言特性可以告诉你在循环内部声明变量和在外部声明变量之间的区别(假设两者都编译)。这在C# 2.0中发生了改变。没有错误。 - Jon Skeet
@JonSkeet 噢,我明白你的意思了,我完全忽略了在1.0中无法像那样关闭变量这一事实,我的错! :) - nawfal

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 Reflector获取的信息,当CIL转换为代码时得到。

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相比,性能差异非常大。还有许多这样的情况,编译器无法优化您的代码,因为它无法弄清楚更大范围内的意图,但是它几乎可以为您优化基本操作。


int j = 0 for (; j < 0x3e8; j++) 这样可以同时声明变量,而不是每个循环都声明。2)赋值比其他选项更快。3)因此,最佳实践规则是在迭代外部进行任何声明。 - luka

23

这是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
这里有一篇关于这个问题的参考资料,来自 Paul Vick:http://www.panopticoncentral.net/archive/2006/03/28/11552.aspx - ferventcoder
1
@eschneider @ferventcoder 很不幸,@PaulV决定删除他的旧博客文章,因此这个链接已经失效了。 - Mark Hurd
是的,最近刚刚发现这个问题;我正在寻找一些官方文档来解决它... - user117499
是的,它不会再次使变量变暗。昨天遇到了这个问题,经过一系列的谷歌搜索才找到了答案。 - smile.al.d.way

18

我做了一个简单的测试:

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编译了这些代码。然后我反汇编了这两个代码的主函数,得到以下结果:

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

对比

第二个

   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

13

它是与语言相关的 - 如果我没记错,C# 会进行优化,因此没有任何区别,但是 JavaScript(例如)每次都会执行整个内存分配过程。


是的,但那并不算什么。我进行了一个简单的测试,使用for循环执行1亿次,发现在声明循环外部时最大的优势只有8毫秒。通常情况下,这个差距更小,大约为3-4毫秒,偶尔在循环外部声明会表现得更糟(多达4毫秒),但这并不是典型情况。 - user137717

11

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

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

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


12
你是否希望变量在整个循环期间都存在,而不是每次迭代分开存在呢?我很少这样做。编写尽可能清晰表达你意图的代码,除非你有一个非常非常好的理由不这样做。 - Jon Skeet
4
啊,好的妥协方案,我从未想过这个!但在我看来,代码变得有点不太直观了。 - Rabarberski
2
@Jon - 我真的不知道OP实际上在使用中间值做什么。只是觉得这是一个值得考虑的选项。 - Kenan Banks

6

在我的看法中,b是更好的结构。在a中,当你的循环结束后,intermediateResult的最后一个值会一直存在。

编辑: 这对于值类型没有太大影响,但是对于引用类型来说可能会有一些负担。个人而言,我喜欢尽早解除变量的引用以进行清理,而b可以为您完成这项工作。


“sticks around after your loop is finished” - 尽管在像Python这样的语言中,绑定名称会一直保留,直到函数结束,所以这并不重要。 - new123456
@new123456:即使问题有点泛泛地提出,但OP要求Java的具体信息。许多源自C的语言都具有块级作用域:C、C++、Perl(使用my关键字)、C#和Java等,这是我使用过的五种语言。 - Powerlord
我知道 - 这只是一种观察,而不是批评。 - new123456

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

这样你只需在循环开始时声明变量一次,离开循环后它就会被销毁。

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