为什么使用提取的方法会导致性能下降?

时间:2015-07-20 06:17:09

标签: c# methods refactoring

在编写一个小程序来比较传统foreach.ToList().ForEach()上的LINQ IEnumerable的性能时,我提取了一个小的虚拟方法,以便能够快速更改操作I想要反对。就在那时我突然注意到我的测量时间有所下降,所以这是我为进一步测试它而创建的一个小类:

class Dummy
{
  public void Iterate()
  {
    Stopwatch sw = Stopwatch.StartNew();

    foreach (int n in Enumerable.Range(0, 50000000))
    {
      int dummy = n / 2;
    }

    sw.Stop();
    Console.WriteLine("Iterate took {0}ms.", sw.ElapsedMilliseconds);
  }

  public void IterateWithMethodCall()
  {
    Stopwatch sw = Stopwatch.StartNew();

    foreach (int n in Enumerable.Range(0, 50000000))
    {
      SomeOperation(n);
    }

    sw.Stop();
    Console.WriteLine("IterateWithMethodCall took {0}ms.", sw.ElapsedMilliseconds);
  }

  private void SomeOperation(int n)
  {
    int dummy = n / 2;
  }
}

这是切入点:

public static void Main(string[] args)
{
  Dummy dummy = new Dummy();
  dummy.Iterate();
  dummy.IterateWithMethodCall();
  Console.ReadKey();
}

我在我的机器上得到的输出如下:

  

迭代耗时534毫秒。

     

IterateWithMethodCall花了1256ms。

这背后的原因是什么?我的猜测是程序必须在每个步骤中“跳转”到SomeOperation方法来执行代码,从而浪费时间,但我想要一个更严格的解释(关于这是如何工作的任何参考链接也欢迎)。

这是否意味着我不应该通过在需要每一点性能时将代码片段提取到更小的方法来重构更复杂的操作?

编辑:我查看了生成的IL代码,并且循环(发布模式)存在差异;也许有人可以解释这个,我自己也做不到。这只是循环的代码,因为其余部分是相同的:

IterateWithMethodCall:

    IL_0017: br.s IL_0027
    // loop start (head: IL_0027)
        IL_0019: ldloc.2
        IL_001a: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
        IL_001f: stloc.1
        IL_0020: ldarg.0
        IL_0021: ldloc.1
        IL_0022: call instance void WithoutStatic.Dummy::SomeOperation(int32)

        IL_0027: ldloc.2
        IL_0028: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
        IL_002d: brtrue.s IL_0019
    // end loop

迭代:

    IL_0017: br.s IL_0024
    // loop start (head: IL_0024)
        IL_0019: ldloc.2
        IL_001a: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
        IL_001f: stloc.1
        IL_0020: ldloc.1
        IL_0021: ldc.i4.2
        IL_0022: div
        IL_0023: pop

        IL_0024: ldloc.2
        IL_0025: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
        IL_002a: brtrue.s IL_0019
    // end loop

1 个答案:

答案 0 :(得分:1)

操作时间的差异很容易解释。每次调用方法时,CLR-Runtime都必须跳转到已编译的CLI代码中的定义,才能执行该方法。但这不是主要的事情。此外,运行时必须在方法调用上创建新范围,其中必须存储每个变量和参数。即使创建和发布范围非常快,在您的范围内,您也可以识别时间。

它们也是调试和发布模式之间的区别。编译器可以识别他是否可以嵌入一个简单的方法,因此代码优化会删除您的方法并直接在循环中替换代码。

希望这有帮助。