public class Group
{
public string Name { get; set; }
}
测试:
List<Group> _groups = new List<Group>();
for (int i = 0; i < 10000; i++)
{
var group = new Group();
group.Name = i + "asdasdasd";
_groups.Add(group);
}
Stopwatch _stopwatch2 = new Stopwatch();
_stopwatch2.Start();
foreach (var group in _groups)
{
var count = _groups.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 = _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快。
答案 0 :(得分:18)
关键在于Where()
的实施,如果可以的话,它会将IEnumerable
投射到List<T>
。请注意构建WhereListIterator
的强制转换(来自通过反射获得的.Net源代码):
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
if (source is List<TSource>) return new WhereListIterator<TSource>((List<TSource>)source, predicate);
return new WhereEnumerableIterator<TSource>(source, predicate);
}
我通过复制(并尽可能简化.Net实现)验证了这一点。
至关重要的是,我实现了Count()
的两个版本 - 一个名为TestCount()
我使用IEnumerable<T>
,另一个名为TestListCount()
,我将可枚举项转换为List<T>
在计算物品之前。
这提供了与Where()
运算符相同的加速比,List<T>
运算符(如上所示)也可以转换为foreach
。
(这应该在未附加调试器的版本构建中尝试。)
这表明,与通过List<T>
表示的相同序列相比,使用IEnumerable<T>
迭代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<Group> groups = new List<Group>();
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<TSource>(IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
int count = 0;
foreach (TSource element in source)
{
if (predicate(element))
count++;
}
return count;
}
public static int TestListCount<TSource>(IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
return testListCount((List<TSource>) source, predicate);
}
private static int testListCount<TSource>(List<TSource> source, Func<TSource, bool> predicate)
{
int count = 0;
foreach (TSource element in source)
{
if (predicate(element))
count++;
}
return count;
}
public static IEnumerable<TSource> TestWhere<TSource>(IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
return new WhereListIterator<TSource>((List<TSource>)source, predicate);
}
}
class WhereListIterator<TSource>: Iterator<TSource>
{
readonly Func<TSource, bool> predicate;
List<TSource>.Enumerator enumerator;
public WhereListIterator(List<TSource> source, Func<TSource, bool> 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<TSource>: IEnumerable<TSource>, IEnumerator<TSource>
{
internal TSource current;
public TSource Current
{
get
{
return current;
}
}
public virtual void Dispose()
{
current = default(TSource);
}
public IEnumerator<TSource> GetEnumerator()
{
return this;
}
public abstract bool MoveNext();
object IEnumerator.Current
{
get
{
return Current;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
void IEnumerator.Reset()
{
throw new NotImplementedException();
}
}
}
会更快。
首先,这是完整的测试代码:
TestCount():
现在,这是为两个关键方法testListCount()
和TestCount()
生成的IL。请注意,这些之间的唯一区别是IEnumerable<T>
正在使用testListCount()
而List<T>
使用的是相同的可枚举,但会转换为其基础TestCount():
.method public hidebysig static int32 TestCount<TSource>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!TSource> source, class [mscorlib]System.Func`2<!!TSource, bool> predicate) cil managed
{
.maxstack 8
.locals init (
[0] int32 count,
[1] !!TSource element,
[2] class [mscorlib]System.Collections.Generic.IEnumerator`1<!!TSource> 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<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<!!TSource>::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<!!TSource>::get_Current()
L_0014: stloc.1
L_0015: ldarg.1
L_0016: ldloc.1
L_0017: callvirt instance !1 [mscorlib]System.Func`2<!!TSource, bool>::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<TSource>(class [mscorlib]System.Collections.Generic.List`1<!!TSource> source, class [mscorlib]System.Func`2<!!TSource, bool> predicate) cil managed
{
.maxstack 8
.locals init (
[0] int32 count,
[1] !!TSource element,
[2] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!!TSource> 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<!0> [mscorlib]System.Collections.Generic.List`1<!!TSource>::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<!!TSource>::get_Current()
L_0015: stloc.1
L_0016: ldarg.1
L_0017: ldloc.1
L_0018: callvirt instance !1 [mscorlib]System.Func`2<!!TSource, bool>::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<!!TSource>::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<!!TSource>
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<!!TSource>::get_Current()
callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
。
在第一种情况下是:
call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator<!!TSource>::get_Current()
call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator<!!TSource>::MoveNext()
在第二种情况下,它是:
{{1}}
重要的是,在第二种情况下,正在进行非虚拟呼叫 - 如果它处于循环中(当然是虚拟呼叫),这可能比虚拟呼叫快得多。
答案 1 :(得分:5)
在我看来,区别在于Linq扩展的编码方式。我怀疑Where
正在List<>
类中使用优化来加速操作,但Count
只是遍历IEnumerable<>
。
如果您执行相同的处理,但使用IEnumerable
,则两种方法都接近,Where
稍慢。
List<Group> _groups = new List<Group>();
for (int i = 0; i < 10000; i++)
{
var group = new Group();
group.Name = i + "asdasdasd";
_groups.Add(group);
}
IEnumerable<Group> _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<TSource>)
案例:
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
if (predicate == null)
{
throw Error.ArgumentNull("predicate");
}
if (source is Enumerable.Iterator<TSource>)
{
return ((Enumerable.Iterator<TSource>)source).Where(predicate);
}
if (source is TSource[])
{
return new Enumerable.WhereArrayIterator<TSource>((TSource[])source, predicate);
}
if (source is List<TSource>)
{
return new Enumerable.WhereListIterator<TSource>((List<TSource>)source, predicate);
}
return new Enumerable.WhereEnumerableIterator<TSource>(source, predicate);
}
计数方法。只需遍历IEnumerable:
public static int Count<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> 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;
}
}
答案 2 :(得分:2)
继Matthew Watson的回答之后:
迭代List<T>
生成call
指令而非callvirt
的原因,就像IEnumerable<T>
一样,是C#foreach
语句是鸭子 - 键入。
C#语言规范第8.8.4节说,编译器'确定类型X是否具有适当的GetEnumerator方法'。这优先于可枚举接口使用。因此,此处的foreach
语句使用List<T>.GetEnumerator
的重载,该重载返回List<T>.Enumerator
而不是返回IEnumerable<T>
或IEnumerable
的版本。
编译器还会检查GetEnumerator
返回的类型是否具有Current
属性和不带参数的MoveNext
方法。对于List<T>.Enumerator
,这些方法不标记为virtual
,因此编译器可以编译直接调用。相反,在IEnumerator<T>
中, virtual
,因此编译器必须生成callvirt
指令。通过虚拟功能表调用的额外开销解释了性能的差异。
答案 3 :(得分:1)
我的猜测:
.Where()使用特殊的“ WhereListIterator ”来迭代元素,而Count()则没有,如Wyatt Earp所示。有趣的是迭代器被标记为“ngenable”:
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public WhereListIterator(List<TSource> source, Func<TSource, bool> predicate)
{
this.source = source;
this.predicate = predicate;
}
这可能意味着“迭代器”部分作为“非托管代码”运行,而Count()作为托管代码运行。我不知道这是否有意义/如何证明,但这是我的0.2cents。
另外,如果你重写Count()来仔细处理List,
你可以使它变得相同甚至更快:
public static class TestExt{
public static int CountFaster<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
if (source == null) throw new Exception();
if (predicate == null) throw new Exception();
if(source is List<TSource>)
{
int finalCount=0;
var list = (List<TSource>)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的人获胜(因为冷启动)。
答案 4 :(得分:0)
根据@Matthew Watson的帖子,我检查了一些行为。在我的例子中&#34; Where&#34;总是返回空集合,因此甚至没有在接口IEnumerable上调用Count(这比在List元素上枚举要慢得多)。我没有添加具有不同名称的所有组,而是添加了具有相同名称的所有项目。然后Count比Count + Method快。这是因为在Count方法中,我们枚举所有项目的接口IEnumerable。如果所有项目都相同,则在Method + Count方法中,&#34; Where&#34;返回整个集合(转换为IEnumerable接口)并调用Count(),因此调用多余或者我可以说 - 它减慢了速度。
总而言之,本例中的具体情况让我得出结论:Method + Where总是更快,但事实并非如此。如果&#34;哪里&#34;返回不比原始集合小得多的集合&#34; Method + Where approach&#34;会慢一些。
答案 5 :(得分:-3)
Sarge Borsch在评论中给出了正确答案,但没有进一步解释。
问题在于,首次运行时必须由JIT编译器将字节码编译为x86。因此,您的测量结合了您想要测试的内容和编译时间。并且由于第二个测试使用的大部分内容都是在第一个测试(列表枚举器,Name属性getter等)中编译的,因此第一个测试会受到编译的影响。
解决方案是做一个预热&#34;:你运行你的代码一次而不做任何措施,通常只需要一次迭代,只需编译。然后你启动秒表并再次运行它,并根据需要进行多次迭代以获得足够长的持续时间(例如一秒)。