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.GetEnumerator的重载,它返回List.Enumerator而不是返回IEnumerableIEnumerable

编译器还会检查GetEnumerator返回的类型是否具有Current属性和不带参数的MoveNext方法。 对于List.Enumerator ,这些方法标记为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等)中编译,因此第一次测试会受到编译的影响。

解决方案是进行“预热”:在没有采取措施的情况下运行代码一次,通常只进行一次迭代,只需编译即可。 然后你启动秒表并再次运行它,以获得足够长的持续时间(例如一秒)。