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代码附加到了我原来的帖子中。我现在接受你的回答,因为我最初的问题已经在这里得到了回答:)