关键是在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();
}
}
}
现在这里是两个关键方法的IL生成代码:TestCount()
和 testListCount()
。请记住,它们之间唯一的区别是 TestCount()
使用了 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()
值得注意的是,在第二种情况下进行的是非虚函数调用,如果它在循环中(当然是在循环中),这比虚函数调用快得多。
counters
是什么类型?我猜是List<bool>
,但这并没有太多意义。另外,我认为Skip(1).Any()
会更快。 - Oliver