为什么使用提取方法时性能会下降?

Why is there a performance drop when using an extracted method?

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

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

我在我的机器上得到的输出是这样的:

Iterate took 534ms.

IterateWithMethodCall took 1256ms.

这背后的原因是什么?我的猜测是程序在执行代码的每一步都必须 "jump" 到 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

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

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

希望对您有所帮助。