【问题标题】:Reference type variable recycling - is a new reference variable created every loop in a loop if declared therein?引用类型变量回收 - 如果在循环中声明,是否会在循环中的每个循环中创建一个新的引用变量?
【发布时间】:2011-11-15 00:43:09
【问题描述】:

如下:

MyObject myVariable;
for(int i = 0; i < objects.Length, i++){
  myVariable = objects[i];
  // do stuff...
}

那么效率更高:

for(int i = 0; i < objects.Length, i++){
  MyObject myVariable = objects[i];
  // do stuff...
}

因为不是每次都创建一个新变量来保存引用? (或者编译器是否足够智能以使用相同的变量)..

(如果创建了一个新变量,它会在堆上分配吗?)

【问题讨论】:

  • 简短回答:不,因为 myVariable 的范围是它所在的方法,尽管在 for 循环中声明了一个新变量来保存引用,但并未创建。

标签: c# oop


【解决方案1】:

不,“变量”的存在几乎完全是为了程序员。通过在方法中声明变量,您不会在运行时创建任何额外的工作。

理论上,当为该方法中声明的每个变量调用方法时,编译器会在堆栈上留出空间。因此,该方法中该变量的存在将比其范围更重要。除非使用 new 关键字,否则不会在堆上分配空间。

实际上,编译器可以识别具有如此短范围的变量,以至于它们可以存储在 CPU 上的寄存器中,而不需要堆栈空间。例如:

var a = b[c];
a.ToString();
// never access "a" again.

... 将等同于:

b[c].ToString();

...因为编译器认识到它只需要存储 b[c] 的结果足够长的时间来调用它的方法,所以它可以只使用 CPU 寄存器而不是使用内存。

因此,在循环inside 中声明变量实际上可能会导致方法为变量分配less 堆栈空间,具体取决于之后可能的逻辑流程。然而,这涉及到对大多数人来说没有任何意义的巨大的微优化。

更新

由于有些人似乎仍然认为在循环中声明变量有一些效果,我想我需要提供证据。在 LINQPad 中键入以下程序。

int j;
for(int i = 0; i < 5; i++)
{
    j = i;
}

...和...

for(int i = 0; i < 5; i++)
{
    int j = i;
}

执行代码,然后进入IL选项卡查看生成的IL代码。这两个程序都是一样的:

IL_0000:  ldc.i4.0    
IL_0001:  stloc.0     
IL_0002:  br.s        IL_0008
IL_0004:  ldloc.0     
IL_0005:  ldc.i4.1    
IL_0006:  add         
IL_0007:  stloc.0     
IL_0008:  ldloc.0     
IL_0009:  ldc.i4.5    
IL_000A:  blt.s       IL_0004

因此,有无可争辩的证据表明这在编译时不会产生任何影响。您将从这两个程序中获得完全相同的编译 IL。

【讨论】:

  • 在循环内声明也可以帮助优化器,因为它现在知道 (1) 变量不在循环外使用,并且 (2) 上一次迭代的值不需要结转到下一次迭代。
  • @Raymond:优化器通常可以知道所有这些,无论局部变量在哪里声明。
  • 是的。这更像是一种“惰性编译器”优化。 (这通常对应于“懒惰的其他开发人员阅读您的代码”。)
  • 在循环内声明也可以帮助其他懒惰的开发人员阅读你的代码,因为他/她现在知道... ;-)
  • @Pedro77:编译器可以判断Math.Pi/180 是一个常量,因此它将用其预先计算的值替换Math.Pi/180。如果您将 声明 (double j) 放在不同的位置,您会发现 IL 代码没有任何区别。如果把assignment (j = ...) 放在不同的地方,IL 代码就会不一样。但是,JIT 编译器很可能会注意到 j 是否从未更改或从未使用过,并将其优化掉。
【解决方案2】:

我对“问题”进行了基准测试并同意 StriplingWarrior。至少在速度方面没有区别。

long start = Time();
long end = Time();
Console.WriteLine("Benchmark Runtime: " + (end - start) + " Microseconds");

for(int k = 0; k < 5; k++)
{
    start = Time();
    int j;
    for (int i = 0; i < 900000000; i++)
    {
        j = i;
    }
    end = Time();
    Console.WriteLine("Benchmark 1: " + (end - start) + " Microseconds");
}

for (int k = 0; k < 5; k++)
{
    start = Time();
    for (int i = 0; i < 900000000; i++)
    {
        int j = i;
    }
    end = Time();
    Console.WriteLine("Benchmark 2: " + (end - start) + " Microseconds");
}

结果:

Benchmark Runtime: 1 Microseconds
Benchmark 1: 1730816 Microseconds
Benchmark 1: 1725885 Microseconds
Benchmark 1: 1725629 Microseconds
Benchmark 1: 1726052 Microseconds
Benchmark 1: 1726121 Microseconds
Benchmark 2: 1725843 Microseconds
Benchmark 2: 1725576 Microseconds
Benchmark 2: 1726233 Microseconds
Benchmark 2: 1725786 Microseconds
Benchmark 2: 1729965 Microseconds

【讨论】:

    【解决方案3】:

    简短的回答,是的。

    长答案,是的,它更快,但除非重复多次,否则几乎不会引起注意。 :-)

    我不确定编译器是否会对其进行优化,但我对此表示怀疑,如果可以,对它有好处,你仍然应该像没有那样编写它,让它成为一个习惯。

    【讨论】:

    • 我不同意这个答案。它不是更快,从编码实践的角度来看也不是最好的。您希望尽可能缩小变量的范围,以避免其他程序员的混淆和冲突。
    • 我的印象是,在循环中声明一个变量将在每次运行循环时运行初始化,从而减慢进程(即使它并不多)。把它放在外面是一样的,但取出初始化部分并简单地分配值。这样会快一点。
    • 在参加编译器课程之前,很多人都有这种印象。一旦你理解了编译器是如何看待事物的,编译器只会为每个方法的变量分配一次堆栈空间是完全有道理的。事实上,它通常会回收它为一个变量分配的空间,如果它可以确定不可能同时需要这些变量,则将其用于方法中的多个变量。
    • 当我说初始化时,我并不完全是指创建一个新变量,我的意思是运行初始化代码。我参加了编译器课程。即使没有创建新变量,也会运行初始化代码。所以从技术上讲它更慢,只是不是因为它被分配了内存空间。但没有什么可以不顾一切的。现在我重新阅读了这个问题,我想我应该已经验证了我的答案,现在基本上是“否”:-)
    • 如果您正在创建一个新对象,它将初始化该对象。如果您将变量设置为具有值(null、常量或其他),它可能会初始化为该变量分配的内存块(假设编译器无法优化它)。但只是在循环中声明一个变量并不会添加任何要在编译时运行的实际代码。我会用更多细节更新我的答案。
    猜你喜欢
    • 2011-10-22
    • 2014-06-06
    • 2018-10-13
    • 1970-01-01
    • 1970-01-01
    • 2012-10-15
    • 2012-02-01
    • 2014-01-10
    • 2022-11-04
    相关资源
    最近更新 更多