for循环内声明的变量会影响循环的性能吗?

12

我已经完成了我的作业,并且发现多次保证在for循环内或外声明变量不会影响性能,而且实际上编译为相同的MSIL。但是我还是进行了一些尝试,并发现将变量声明移到循环内部确实会导致明显且稳定的性能提升。

我编写了一个小型的控制台测试类来衡量这种效果。我初始化了一个静态的double[] 数组 items,并且有两个方法对其进行循环操作,将结果写入到另一个静态的double[]数组 buffer 中。最初,我通过观察差异发现这种变化的方法是复数的模运算。对于长度为1000000的items数组进行100次循环,我得到的运行时间在将变量(6个double变量)放在循环内部的方法中始终更短:例如,在老式Intel Core 2 Duo @2.66 GHz配置下,32.83±0.64 ms v 43.24±0.45 ms。我尝试按不同的顺序执行它们,但没有影响结果。

然后我意识到计算复数的模远非最简单的工作示例,并测试了两种简单得多的方法:

    static void Square1()
    {
        double x;

        for (int i = 0; i < buffer.Length; i++) {
            x = items[i];
            buffer[i] = x * x;
        }
    }


    static void Square2()
    {
        for (int i = 0; i < buffer.Length; i++) {
            double x;
            x = items[i];
            buffer[i] = x * x;
        }
    }

有了这些,结果就出现了反向情况:在循环外声明变量似乎更为有利:Square1() 7.07±0.43 毫秒,Square2() 12.07±0.51 毫秒。

我不熟悉ILDASM,但我已经反汇编了这两种方法,唯一的区别似乎只是本地变量的初始化:

      .locals init ([0] float64 x,
       [1] int32 i,
       [2] bool CS$4$0000)

Square1() 函数中,v

      .locals init ([0] int32 i,
       [1] float64 x,
       [2] bool CS$4$0000)

Square2()中。根据它,一个方法中的stloc.1在另一个方法中是stloc.0,反之亦然。在更长的复杂数值计算MSIL代码中,甚至代码大小也不同,在外部声明代码中出现了stloc.s i,而在内部声明代码中出现了stloc.0

那么这是怎么回事呢?我有遗漏什么吗,还是这是一个真正的影响?如果是的话,在长循环的性能方面可能会产生显着差异,所以我认为它值得一些讨论。

非常感谢您的想法。

编辑:我忽视的唯一一件事是在发布之前在几台电脑上进行测试。我现在已经在i5上运行了它,两种方法的结果几乎相同。非常抱歉我发表了这样一个具有误导性的观察。


1
好的调查,你肯定会得到一枚赞。 - NicoRiff
2
@NicoRiff:确实,这是一个非常好的问题。(不过,我认为答案很简单。) - Bathsheba
1
我迫不及待地期待着@JonSkeet对此的回答。 - NicoRiff
1
我无法使用给定的代码复制此行为。生成的 IL 明确翻转了局部变量的声明顺序,但我没有看到任何显着的性能差异。 - Kyle
1
你能展示一下你用来测量性能的代码吗?你考虑过第一次运行代码时JIT编译的影响吗? - Chris Dunaway
显示剩余2条评论
2个回答

8
任何一款值得一试的C#编译器都会为您执行这样的微优化。只有在必要时才将变量泄漏到外部作用域。
因此,如果可能的话,请将“double x;”保持在循环内部。
但是,如果“items[i]”是普通数据数组访问,则我会编写“buffer[i] = items[i] * items[i];”。C和C++会进行优化,但我认为C#还没有(至今);您的反汇编代码表明它不会。

非常感谢!我曾经沉迷于在方法开始时声明所有变量的习惯,但从现在开始我会三思而后行。我的收获是如果我关心性能,那么测试两种安排都很重要,因为优化似乎可以朝着两个方向发展。 - tethered.sun
1
毛茸茸的答案是:“多年的经验告诉你,松散作用域的变量最终会导致代码库的混乱。” - Bathsheba
你的回答似乎表明在循环内部和外部保持变量没有性能差异,但这并不能真正解释OP所经历的测量差异。 - HugoRune
我滑稽地认为这完全是由于弹性尺的概念所导致的。 - Bathsheba
C# 编译器偶尔会删除局部变量。我不确定它在什么情况下能够做到这一点,但我之前见过它这样做。 - Kyle

1

对于这两种情况,分析垃圾回收器的工作将会很有趣。

我可以想象,在第一种情况下,变量x在循环运行时不会被收集,因为它是在外部范围中声明的。

在第二种情况下,每次迭代都会删除x上的所有句柄。

也许你可以使用新的C# 4.6 GC.TryStartNoGCRegionGC.EndNoGCRegion再次运行测试,以查看性能影响是否来自垃圾回收。

防止.NET短时间内进行垃圾回收


谢谢,这是一个很好的想法。我本来想已经测试过了,但目前我没有访问.NET 4.6的权限。SharpDevelop似乎不支持它。我会尝试升级我的工具并回答这个问题。 - tethered.sun
1
我怀疑这与GC无关。double是值类型,在这种情况下,将被分配到堆栈中。它不会产生任何垃圾需要清理。 - Kyle
2
Eric Lippert在https://dev59.com/8GzXa4cB1Zd3GeqPUYFW#14043763上提供了一些非常好的信息。 - Bradley Uffner

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