LINQ性能计数vs Where和Count
public class Group { public string Name { get; set; } }
测试:
List _groups = new List(); for (int i = 0; i x.Name == group.Name); } _stopwatch2.Stop(); Console.WriteLine(_stopwatch2.ElapsedMilliseconds); Stopwatch _stopwatch = new Stopwatch(); _stopwatch.Start(); foreach (var group in _groups) { var count = _groups.Where(x => x.Name == group.Name).Count(); } _stopwatch.Stop(); Console.WriteLine(_stopwatch.ElapsedMilliseconds);
结果:第一名:2863,第二名2185
有人可以解释一下为什么第一种方法比第二种方法慢? 第二个应该返回枚举器并调用它的计数,然后首先调用count。 第一种方法应该快一点。
编辑:我删除了计数器列表,以防止使用GC和更改顺序来检查订单是否有意义。 结果几乎相同。
EDIT2:此性能问题仅与Count无关。 它与First(),FirstOrDefault(),Any()等有关。其中+ Method总是比Method快。
关键是在Where()
的实现中,如果可以的话,它将IEnumerable
转换为List
。 注意构造WhereListIterator
(这是来自通过reflection获得的.Net源代码):
public static IEnumerable Where (this IEnumerable source, Func predicate) { if (source is List ) return new WhereListIterator ((List )source, predicate); return new WhereEnumerableIterator (source, predicate); }
我通过复制(并尽可能简化).Net实现来validation这一点。
至关重要的是,我实现了两个版本的Count()
– 一个名为TestCount()
,其中我使用IEnumerable
,另一个名为TestListCount()
,其中我在计算项目之前将枚举转换为List
。
这给出了与Where()
运算符相同的加速比,其中(如上所示Where()
运算符也可以转换为List
。
(这应该在没有附加调试器的版本构建中尝试。)
这表明,与通过IEnumerable
表示的相同序列相比,使用foreach
迭代List
更快。
首先,这是完整的测试代码:
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace Demo { public class Group { public string Name { get; set; } } internal static class Program { static void Main() { int dummy = 0; List groups = new List (); for (int i = 0; i < 10000; i++) { var group = new Group(); group.Name = i + "asdasdasd"; groups.Add(group); } Stopwatch stopwatch = new Stopwatch(); for (int outer = 0; outer < 4; ++outer) { stopwatch.Restart(); foreach (var group in groups) dummy += TestWhere(groups, x => x.Name == group.Name).Count(); Console.WriteLine("Using TestWhere(): " + stopwatch.ElapsedMilliseconds); stopwatch.Restart(); foreach (var group in groups) dummy += TestCount(groups, x => x.Name == group.Name); Console.WriteLine("Using TestCount(): " + stopwatch.ElapsedMilliseconds); stopwatch.Restart(); foreach (var group in groups) dummy += TestListCount(groups, x => x.Name == group.Name); Console.WriteLine("Using TestListCount(): " + stopwatch.ElapsedMilliseconds); } Console.WriteLine("Total = " + dummy); } public static int TestCount(IEnumerable source, Func predicate) { int count = 0; foreach (TSource element in source) { if (predicate(element)) count++; } return count; } public static int TestListCount (IEnumerable source, Func predicate) { return testListCount((List ) source, predicate); } private static int testListCount (List source, Func predicate) { int count = 0; foreach (TSource element in source) { if (predicate(element)) count++; } return count; } public static IEnumerable TestWhere (IEnumerable source, Func predicate) { return new WhereListIterator ((List )source, predicate); } } class WhereListIterator : Iterator { readonly Func predicate; List .Enumerator enumerator; public WhereListIterator(List source, Func predicate) { this.predicate = predicate; this.enumerator = source.GetEnumerator(); } public override bool MoveNext() { while (enumerator.MoveNext()) { TSource item = enumerator.Current; if (predicate(item)) { current = item; return true; } } Dispose(); return false; } } abstract class Iterator : IEnumerable , IEnumerator { internal TSource current; public TSource Current { get { return current; } } public virtual void Dispose() { current = default(TSource); } public IEnumerator GetEnumerator() { return this; } public abstract bool MoveNext(); object IEnumerator.Current { get { return Current; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } void IEnumerator.Reset() { throw new NotImplementedException(); } } }
现在这里是为两个关键方法生成的IL, TestCount():
和testListCount()
。 请记住,这些之间的唯一区别是TestCount()
使用IEnumerable
并且testListCount()
使用相同的可枚举,但testListCount()
转换为其基础List
类型:
TestCount(): .method public hidebysig static int32 TestCount(class [mscorlib]System.Collections.Generic.IEnumerable`1 source, class [mscorlib]System.Func`2 predicate) cil managed { .maxstack 8 .locals init ( [0] int32 count, [1] !!TSource element, [2] class [mscorlib]System.Collections.Generic.IEnumerator`1 CS$5$0000) L_0000: ldc.i4.0 L_0001: stloc.0 L_0002: ldarg.0 L_0003: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1 [mscorlib]System.Collections.Generic.IEnumerable`1::GetEnumerator() L_0008: stloc.2 L_0009: br L_0025 L_000e: ldloc.2 L_000f: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1::get_Current() L_0014: stloc.1 L_0015: ldarg.1 L_0016: ldloc.1 L_0017: callvirt instance !1 [mscorlib]System.Func`2::Invoke(!0) L_001c: brfalse L_0025 L_0021: ldloc.0 L_0022: ldc.i4.1 L_0023: add.ovf L_0024: stloc.0 L_0025: ldloc.2 L_0026: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() L_002b: brtrue.s L_000e L_002d: leave L_003f L_0032: ldloc.2 L_0033: brfalse L_003e L_0038: ldloc.2 L_0039: callvirt instance void [mscorlib]System.IDisposable::Dispose() L_003e: endfinally L_003f: ldloc.0 L_0040: ret .try L_0009 to L_0032 finally handler L_0032 to L_003f } testListCount(): .method private hidebysig static int32 testListCount (class [mscorlib]System.Collections.Generic.List`1 source, class [mscorlib]System.Func`2 predicate) cil managed { .maxstack 8 .locals init ( [0] int32 count, [1] !!TSource element, [2] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator CS$5$0000) L_0000: ldc.i4.0 L_0001: stloc.0 L_0002: ldarg.0 L_0003: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator [mscorlib]System.Collections.Generic.List`1::GetEnumerator() L_0008: stloc.2 L_0009: br L_0026 L_000e: ldloca.s CS$5$0000 L_0010: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator::get_Current() L_0015: stloc.1 L_0016: ldarg.1 L_0017: ldloc.1 L_0018: callvirt instance !1 [mscorlib]System.Func`2::Invoke(!0) L_001d: brfalse L_0026 L_0022: ldloc.0 L_0023: ldc.i4.1 L_0024: add.ovf L_0025: stloc.0 L_0026: ldloca.s CS$5$0000 L_0028: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator::MoveNext() L_002d: brtrue.s L_000e L_002f: leave L_0042 L_0034: ldloca.s CS$5$0000 L_0036: constrained [mscorlib]System.Collections.Generic.List`1/Enumerator L_003c: callvirt instance void [mscorlib]System.IDisposable::Dispose() L_0041: endfinally L_0042: ldloc.0 L_0043: ret .try L_0009 to L_0034 finally handler L_0034 to L_0042 }
我认为这里的重要部分是调用IEnumerator::GetCurrent()
和IEnumerator::MoveNext()
。
在第一种情况下,它是:
callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1::get_Current() callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
在第二种情况下,它是:
call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator::get_Current() call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator::MoveNext()
重要的是,在第二种情况下,正在进行非虚拟呼叫 – 如果它处于循环中(当然是虚拟呼叫),则可以明显快于虚拟呼叫。
在我看来,区别在于Linq扩展的编码方式。 我怀疑Where
List<>
类中使用优化来加速操作,但Count
只是迭代IEnumerable<>
。
如果您执行相同的过程,但使用IEnumerable
,则两种方法都接近,其中Where
稍微慢一些。
List _groups = new List (); for (int i = 0; i < 10000; i++) { var group = new Group(); group.Name = i + "asdasdasd"; _groups.Add(group); } IEnumerable _groupsEnumerable = from g in _groups select g; Stopwatch _stopwatch2 = new Stopwatch(); _stopwatch2.Start(); foreach (var group in _groups) { var count = _groupsEnumerable.Count(x => x.Name == group.Name); } _stopwatch2.Stop(); Console.WriteLine(_stopwatch2.ElapsedMilliseconds); Stopwatch _stopwatch = new Stopwatch(); _stopwatch.Start(); foreach (var group in _groups) { var count = _groupsEnumerable.Where(x => x.Name == group.Name).Count(); } _stopwatch.Stop(); Console.WriteLine(_stopwatch.ElapsedMilliseconds);
哪里有扩展方法。 注意if (source is List
情况:
public static IEnumerable Where (this IEnumerable source, Func predicate) { if (source == null) { throw Error.ArgumentNull("source"); } if (predicate == null) { throw Error.ArgumentNull("predicate"); } if (source is Enumerable.Iterator ) { return ((Enumerable.Iterator )source).Where(predicate); } if (source is TSource[]) { return new Enumerable.WhereArrayIterator ((TSource[])source, predicate); } if (source is List ) { return new Enumerable.WhereListIterator ((List )source, predicate); } return new Enumerable.WhereEnumerableIterator (source, predicate); }
计数方法。 只需遍历IEnumerable:
public static int Count(this IEnumerable source, Func predicate) { if (source == null) { throw Error.ArgumentNull("source"); } if (predicate == null) { throw Error.ArgumentNull("predicate"); } int num = 0; checked { foreach (TSource current in source) { if (predicate(current)) { num++; } } return num; } }
我猜:
.Where()使用特殊的“ WhereListIterator ”迭代元素,Count()不会,如Wyatt Earp所示。 有趣的是迭代器被标记为“ngenable”:
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] public WhereListIterator(List source, Func predicate) { this.source = source; this.predicate = predicate; }
这可能意味着“迭代器”部分作为“非托管代码”运行,而Count()作为托管代码运行。 我不知道这是否有意义/如何certificate,但这是我的0.2cents。
此外,如果您重写Count()以仔细处理List,
你可以使它变得相同甚至更快:
public static class TestExt{ public static int CountFaster(this IEnumerable source, Func predicate) { if (source == null) throw new Exception(); if (predicate == null) throw new Exception(); if(source is List ) { int finalCount=0; var list = (List )source; var count = list.Count; for(var j = 0; j < count; j++){ if(predicate(list[j])) finalCount++; } return finalCount; } return source.Count(predicate); }
}
在我的测试; 在我开始使用CountFaster()之后,被称为LATER的人获胜(因为冷启动)。
接下来是Matthew Watson的回答:
迭代List
的原因是生成call
指令而不是用于IEnumerable
callvirt
,因为C# foreach
语句是鸭子类型的。
C#语言规范第8.8.4节说,编译器’确定类型X是否具有适当的GetEnumerator方法’。 这优先于可枚举接口使用。 因此, foreach
语句使用List
的重载,它返回List
而不是返回IEnumerable
或IEnumerable
。
编译器还会检查GetEnumerator
返回的类型是否具有Current
属性和不带参数的MoveNext
方法。 对于List
,这些方法未标记为virtual
,因此编译器可以编译直接调用。 相反,在IEnumerator
它们是 virtual
因此编译器必须生成callvirt
指令。 通过虚拟function表调用的额外开销解释了性能的差异。
根据@Matthew Watson的post,我检查了一些行为。 在我的示例中,“Where”总是返回空集合,因此甚至没有在接口IEnumerable上调用Count(这比在List元素上枚举要慢得多)。 我没有添加具有不同名称的所有组,而是添加了具有相同名称的所有项目。 然后Count比Count + Method快。 这是因为在Count方法中,我们枚举所有项目的接口IEnumerable。 在Method + Count方法中,如果所有项都相同,“Where”返回整个集合(转换为IEnumerable接口)并调用Count(),因此Where调用是多余的,或者我可以说 – 它会减慢速度。
总而言之,这个例子中的具体情况让我得出结论,Method + Where总是更快,但事实并非如此。 如果“Where”返回的集合不比原始集合“Method + Where approach”慢得多。
Sarge Borsch在评论中给出了正确答案,但没有进一步解释。
问题在于第一次运行时必须由JIT编译器将字节码编译为x86。 因此,您的测量结合了您想要测试的内容和编译时间。 并且由于第二次测试使用的大部分内容都将在第一次测试(列表枚举器,Name属性getter等)中编译,因此第一次测试会受到编译的影响。
解决方案是进行“预热”:在没有采取措施的情况下运行代码一次,通常只进行一次迭代,只需编译即可。 然后你启动秒表并再次运行它,以获得足够长的持续时间(例如一秒)。