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

C# 为什么使用提取方法时会出现性能下降?,c#,methods,refactoring,C#,Methods,Refactoring,在编写一个小程序来比较传统的foreach与IEnumerable上的LINQ.ToList().foreach()的性能时,我提取了一个小的虚拟方法,以便能够快速更改要测试的操作。就在那时,我突然注意到我测量的时间减少了,所以我创建了一个小类来进一步测试它: class Dummy { public void Iterate() { Stopwatch sw = Stopwatch.StartNew(); foreach (int n in Enumerable.Ra

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

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();
}
我在机器上获得的输出如下所示:

迭代时间为534ms

迭代该方法调用了1256ms

这背后的原因是什么?我的猜测是,程序必须在每一步中“跳转”到
SomeOperation
方法来执行代码,因此会浪费时间,但我希望得到更严格的解释(也欢迎提供有关该方法具体工作原理的任何参考链接)

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

编辑:我查看了生成的IL代码,循环中存在差异(释放模式);也许有人能解释这个,我自己无法解释。这只是循环的代码,其余代码相同:

迭代方法调用:

    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_0027
//环路启动(磁头:IL_0027)
IL_0019:ldloc.2
IL_001a:callvirt实例!0类[mscorlib]System.Collections.Generic.IEnumerator`1::get_Current()
IL_001f:stloc.1
IL_0020:ldarg.0
IL_0021:ldloc.1
IL_0022:调用实例void WithoutStatic.Dummy::SomeOperation(int32)
IL_0027:ldloc.2
IL_0028:callvirt实例bool[mscorlib]System.Collections.IEnumerator::MoveNext()
IL_002d:brtrue.s IL_0019
//端环
迭代:

    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
IL_0017:br.s IL_0024
//环路启动(磁头:IL_0024)
IL_0019:ldloc.2
IL_001a:callvirt实例!0类[mscorlib]System.Collections.Generic.IEnumerator`1::get_Current()
IL_001f:stloc.1
IL_0020:ldloc.1
IL_0021:ldc.i4.2
IL_0022:分区
伊卢0023:流行音乐
IL_0024:ldloc.2
IL_0025:callvirt实例bool[mscorlib]System.Collections.IEnumerator::MoveNext()
IL_002a:brtrue.s IL_0019
//端环

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

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


希望这有帮助。

这是在发布模式还是调试模式?啊,是的,我在执行时间时忘记了更改为发布模式(我经常忘记这个…)。当我切换到释放模式时,我获得了很多,但仍然有大约150毫秒的间隔。即使将代码嵌入循环中,编译器是否仍会创建一个新的作用域?不,他不会,这是它速度更快的原因之一。但是,在发布模式下,它的执行时间不应该大致相同,而不是不同吗?还是我在什么地方误解了你?也就是说,为什么仍然存在差距?有趣的一点是,查看生成的CIL代码。我稍后再看,或者你试着把它贴在这里。我把IL代码附加到了我原来的帖子中。我现在接受你的回答,因为我最初的问题已经在这里得到了回答:)