为什么使用提取方法时性能会下降?
Why is there a performance drop when using an extracted method?
在编写一个小程序来比较传统 foreach
与 IEnumerable
上的 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 代码中的定义,以执行该方法。但这不是主要的。此外,运行时必须在方法调用上创建一个新范围,其中必须存储每个变量和参数。即使创建和释放范围非常非常快,在您的范围内您也可以识别时间。
它们也是调试和发布模式之间的区别。编译器可以识别他是否可以嵌入一个简单的方法,所以代码优化删除你的方法并直接替换你循环中的代码。
希望对您有所帮助。
在编写一个小程序来比较传统 foreach
与 IEnumerable
上的 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 代码中的定义,以执行该方法。但这不是主要的。此外,运行时必须在方法调用上创建一个新范围,其中必须存储每个变量和参数。即使创建和释放范围非常非常快,在您的范围内您也可以识别时间。
它们也是调试和发布模式之间的区别。编译器可以识别他是否可以嵌入一个简单的方法,所以代码优化删除你的方法并直接替换你循环中的代码。
希望对您有所帮助。