繁体   English   中英

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

[英]Difference between declaring variables before or in loop?

我一直想知道,一般来说,在循环之前声明一个丢弃变量,而不是在循环内重复,是否会产生任何(性能)​​差异? 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 案例特别感兴趣。

ab哪个更好?

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

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

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

A:平均执行时间:0.074 秒

B:平均执行时间:0.067 秒

令我惊讶的是,B 的速度稍快一些。 与计算机一样快,现在很难说您是否可以准确地测量这一点。 我也会以 A 方式对其进行编码,但我会说这并不重要。

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

以下是我在 .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/字节码,并在执行时转换为机器语言。 因此,在机器语言中,甚至可能不会在堆栈上创建双精度值。 它可能只是一个寄存器,因为代码反映它是WriteLine函数的临时变量。 对于循环,有一整套优化规则。 所以一般人不应该担心它,尤其是在托管语言中。 在某些情况下,您可以优化管理代码,例如,如果您必须仅使用string a; a+=anotherstring[i]连接大量string a; a+=anotherstring[i] string a; a+=anotherstring[i]与使用StringBuilder 两者在性能上有很大差异。 有很多这样的情况,编译器无法优化您的代码,因为它无法弄清楚更大范围内的意图。 但它几乎可以为您优化基本的东西。

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

但是,如果添加 a = 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...

我做了一个简单的测试:

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

对比

   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 

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

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

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

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

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

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

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

如果您在 lambda 等中使用变量,则 C# 中存在差异。但总的来说,编译器基本上会做同样的事情,假设变量仅在循环中使用。

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

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

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

作为一般规则,我在最内部的可能范围内声明我的变量。 因此,如果您不在循环之外使用中间结果,那么我会选择 B。

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

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

  • 它将变量的范围缩小到需要的地方,这是一件好事。
  • Java 进行了足够的优化,不会对性能产生显着影响。 IIRC,也许第二种形式更快。

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

好吧,你总是可以为它做一个范围:

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

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

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

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

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

但是,如果你有这个:

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

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

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

我的做法如下:

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

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

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

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

外循环案例

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

从性能的角度来看,外部(好多)更好。

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 亿次。 external() 花费了 65 毫秒。 inside() 花了 1.5 秒。

如果有人感兴趣,我用 Node 4.0.0 测试了 JS。 在循环外声明导致平均超过 1000 次试验(每次试验 1 亿次循环迭代)的性能提高约 0.5 毫秒。 所以我要说继续以最易读/可维护的方式编写它,即 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)

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

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

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

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

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

这是更好的形式

double intermediateResult;
int i = byte.MinValue;

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

1)以这种方式声明一次时间既变量,又不是每个for循环。 2)分配它比所有其他选项更胖。 3) 所以最佳实践规则是迭代之外的任何声明。

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

零差异,根据汇编器输出。

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM