我查看了.NET Framework源代码,偶然发现了implementation of LINQ-Sum
int Sum(this IEnumerable<int> source)
我看到它是用foreach循环实现的,并且想知道为什么MS的人不会因为性能原因而使用正常的for循环(后来我才知道,for-之间的性能差异不再存在 - 循环和foreach循环 - 但直到现在我才知道。
所以我在我自己的项目中复制了MS实现并编写了一些基准:
var range = Enumerable.Range(1, 1000);
Stopwatch sw = new Stopwatch();
//Do sth unimportant for warming up
sw.Start();
for(int i = 0; i <= 10000; i++)
{
long z = i + 3;
}
sw.Stop();
//Implementation 1
sw.Reset();
sw.Start();
for (int i = 0; i <= 1000000; i++)
{
long i1 = range.Sum1();
}
sw.Stop();
Console.WriteLine("Sum1: " + sw.ElapsedTicks.ToString());
//Implementation 2
sw.Reset();
sw.Start();
for (int i = 0; i <= 1000000; i++)
{
long i2 = range.Sum2();
}
sw.Stop();
Console.WriteLine("Sum2: " + sw.ElapsedTicks.ToString());
以下是Sum的两个实现(注意:两者都是相同的,我首先要检查测量是否正常工作):
public static class LinqExtension
{
public static int Sum1(this IEnumerable<int> source)
{
int sum = 0;
checked
{
foreach (int v in source) sum += v;
}
return sum;
}
public static int Sum2(this IEnumerable<int> source)
{
int sum = 0;
checked
{
foreach (int v in source) sum += v;
}
return sum;
}
}
令人惊讶的是我得到了两个不同的结果:Sum1 = 16043441 vs. Sum2 = 17480907
所以我稍微扩展了基准测试并且不仅仅调用了Sum1和Sum2一次,而是按以下顺序多次调用:
因此Sum1总是比Sum2快近10%。当我首先调用Sum2时,结果是相反的。
导致这些性能差异的原因是什么?为什么第一个被调用的方法比第二个更快?我的基准无效吗?
我正在使用Visual Studio 2015 CTP4和.NET Framework 4.5.3
编辑:
结果以毫秒而不是刻度
感谢评论,我修正了一些错误,现在代码看起来像这样:
sw.Start();
for (int i = 0; i <= 1000000; i++)
{
i1 = range.Sum1();
}
sw.Stop();
Console.WriteLine("Sum1: " + sw.ElapsedMilliseconds.ToString() + "\n" + i1.ToString());
现在结果完全不同了:
但是仍然存在差异,但现在反过来了。
另一个更新:
当我使用
时int[] range = new int[1000];
for (int m = 0; m < range.Length; m++)
range[m] = m+1;
而不是
var range = Enumerable.Range(1, 1000);
这两种方法同样快。
更新: 用Mono(SharpDevelop)和VS2013测试它,我得到了完全一致的结果。所以我认为使用VS2015不是一个好主意,因为它仍然是一个测试版。因此,结果的重要性非常低。
另一个更新:
stakx评论道:
尝试至少调用一次
Sum1
和Sum2
方法 在你开始测量时间之前,为了确保 方法的代码由JIT生成。否则你可能会 包括你的JIT代码生成所需的时间 基准
所以我在测量前一次调用了Sum1
和Sum2
,令人惊讶的是这解决了问题。但我不明白为什么。我知道JIT生成代码会花费一些时间,但这只是第一次。在我的测试中,我有20个for循环,每个循环分别调用Sum1和Sum2 1.000.000次。我对每个循环进行测量,并为Sum1和Sum2获得不同的值。
如果第一个循环较慢,那将是有意义的,但事实并非如此。
我使用ngen.exe生成原生图像并得到以下结果:
所以仍然存在这种差异。
非常重要:它并不总是第一种更快的方法!有时 它是第一个被调用的方法,有时是第二个。但是一旦组装完成,结果就是可重复的。 这对我来说很混乱,当发生这种情况时,我看不到任何模式。
Enigmativity:
您是否尝试过交换调用方法的顺序? 先拨打
Sum2
?
是的,但结果只是反过来。如果Sum1
是“快速方法”,则在交换后,Sum2
是快速的,Sum1
是慢速的。
答案 0 :(得分:1)
我稍微修改了你的测试代码,我发现有两个因素对性能有影响:你是否运行相同或两个不同的集合以及枚举的类型(我不知道为什么尚未)。
枚举List<int>
似乎是最慢的情况。数组int[]
是最快的数组,当你使用两个不同的范围时,两者之间实际上没有区别(但是当使用列表时总是存在差异):
static void Main(string[] args)
{
// Try with .ToList() and .ToArray()
var range1 = Enumerable.Range(1, 1000);
var range2 = Enumerable.Range(1, 1000);
int numberOfSums = 100000;
int numberOfTests = 3;
for (int i = 0; i < numberOfTests; i++)
{
SumBenchmark(range1, LinqExtension.Sum1, numberOfSums, "Sum1");
}
for (int i = 0; i < numberOfTests; i++)
{
// Also try with range1
SumBenchmark(range2, LinqExtension.Sum2, numberOfSums, "Sum2");
}
Console.ReadKey();
}
static void SumBenchmark(IEnumerable<int> numbers, Func<IEnumerable<int>, int> sum, int numberOfSums, string name)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < numberOfSums; i++)
{
long result = sum(numbers);
}
sw.Stop();
Console.WriteLine("{2}: {0} ticks in {1} ms ", sw.ElapsedTicks.ToString(), sw.ElapsedMilliseconds.ToString(), name);
}
对于我来说,第二次通话总是更快。
编辑:如果您在构建设置中禁用Prefer 32-bit
选项并将其编译为64-bit
,则会产生巨大的性能影响 - 然后原始Sum
运行得更快:
...但是它运行时的速度没有.ToList()
和.ToArray()
EDIT-2:这是Sum2
使用int[]
而不是IEnumerable
的另一个结果:
Sum1: in 878 ms
Sum1: in 863 ms
Sum1: in 875 ms
Sum2: in 122 ms
Sum2: in 122 ms
Sum2: in 121 ms
Linq: in 830 ms
Linq: in 825 ms
Linq: in 836 ms
生成的IL
也不同:
的
public static int Sum2(this int[] source)
它的
.method public hidebysig static int32 Sum2(int32[] source) cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 )
// Code size 28 (0x1c)
.maxstack 2
.locals init ([0] int32 sum,
[1] int32 v,
[2] int32[] CS$6$0000,
[3] int32 CS$7$0001)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldarg.0
IL_0003: stloc.2
IL_0004: ldc.i4.0
IL_0005: stloc.3
IL_0006: br.s IL_0014
IL_0008: ldloc.2
IL_0009: ldloc.3
IL_000a: ldelem.i4
IL_000b: stloc.1
IL_000c: ldloc.0
IL_000d: ldloc.1
IL_000e: add.ovf
IL_000f: stloc.0
IL_0010: ldloc.3
IL_0011: ldc.i4.1
IL_0012: add
IL_0013: stloc.3
IL_0014: ldloc.3
IL_0015: ldloc.2
IL_0016: ldlen
IL_0017: conv.i4
IL_0018: blt.s IL_0008
IL_001a: ldloc.0
IL_001b: ret
} // end of method LinqExtension::Sum2
和
public static int Sum1(this IEnumerable<int> source)
它的
.method public hidebysig static int32 Sum1(class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> source) cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 )
// Code size 44 (0x2c)
.maxstack 2
.locals init ([0] int32 sum,
[1] int32 v,
[2] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> CS$5$0000)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldarg.0
IL_0003: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
IL_0008: stloc.2
.try
{
IL_0009: br.s IL_0016
IL_000b: ldloc.2
IL_000c: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
IL_0011: stloc.1
IL_0012: ldloc.0
IL_0013: ldloc.1
IL_0014: add.ovf
IL_0015: stloc.0
IL_0016: ldloc.2
IL_0017: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_001c: brtrue.s IL_000b
IL_001e: leave.s IL_002a
} // end .try
finally
{
IL_0020: ldloc.2
IL_0021: brfalse.s IL_0029
IL_0023: ldloc.2
IL_0024: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0029: endfinally
} // end handler
IL_002a: ldloc.0
IL_002b: ret
} // end of method LinqExtension::Sum1
在所有迭代之后IEnumerable
参数不会太快......并且仅当foreach
循环的集合不是IEnumerable
时才进行优化。 DEMO