LINQ性能计数vs Where和Count

时间:2014-09-11 12:32:30

标签: c# linq

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快。

6 个答案:

答案 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;:你运行你的代码一次而不做任何措施,通常只需要一次迭代,只需编译。然后你启动秒表并再次运行它,并根据需要进行多次迭代以获得足够长的持续时间(例如一秒)。

相关问题