Array.Count()比List.Count()慢得多

时间:2013-04-25 07:37:28

标签: c# .net performance linq ienumerable

使用IEnumerable<T> Count()的扩展方法时,数组的速度至少比列表慢两倍。

Function                      Count()
List<int>                     2,299
int[]                         6,903

差异来自哪里?

我知道两者都在调用Count的{​​{1}}属性:

  

如果源的类型实现ICollection,则该实现用于获取元素的数量。否则,此方法确定计数。

对于列表,它返回List<T>.Count,对于数组,ICollection。此外,Array.Length应该比Array.Length更快。

基准:

List<T>.Count

编辑:

leppieKnaģis的答案非常惊人,但我想补充一句话 As Jon Skeet said:

  

实际上有两个等效的块,只是测试   不同的集合接口类型,并使用它找到的任何一种   首先(如果有的话)。我不知道.NET实现是否测试   ICollection或ICollection&lt; T>首先 - 我可以通过实施来测试它   两个接口,但每个接收不同的计数,当然,   但这可能是矫枉过正的。这并不重要   除了轻微的性能差异之外,表现良好的收藏品    - 我们首先要测试“最有可能”的界面,我认为这是通用界面。

泛型可能是最有可能发生的,但是如果你反转这两个,即在泛型之前调用非泛型强制转换,则Array.Count()变得比List.Count()快一点。另一方面,List的非通用版本较慢。

很高兴知道是否有人想在1e8迭代循环中调用class Program { public const long Iterations = (long)1e8; static void Main() { var list = new List<int>(){1}; var array = new int[1]; array[0] = 1; var results = new Dictionary<string, TimeSpan>(); results.Add("List<int>", Benchmark(list, Iterations)); results.Add("int[]", Benchmark(array, Iterations)); Console.WriteLine("Function".PadRight(30) + "Count()"); foreach (var result in results) { Console.WriteLine("{0}{1}", result.Key.PadRight(30), Math.Round(result.Value.TotalSeconds, 3)); } Console.ReadLine(); } public static TimeSpan Benchmark(IEnumerable<int> source, long iterations) { var countWatch = new Stopwatch(); countWatch.Start(); for (long i = 0; i < iterations; i++) source.Count(); countWatch.Stop(); return countWatch.Elapsed; } }

Count()

4 个答案:

答案 0 :(得分:9)

原因是Enumerable.Count<T>()执行转换为ICollection<T>以从列表和数组中检索计数。

使用此示例代码:

public static int Count<TSource>(IEnumerable<TSource> source)
{
    ICollection<TSource> collection = source as ICollection<TSource>;
    if (collection != null)
    {
        return 1; // collection.Count;
    }
}

你可以确定演员阵容需要花费更长的时间,实际上大部分时间用于计算:

Function                      Count()
List<int>                     1,575
int[]                         5,069

关键可能是来自documentation(强调我的)的声明:

  

在.NET Framework 2.0版中,Array类实现System.Collections.Generic.IList,   System.Collections.Generic.ICollection,和   System.Collections.Generic.IEnumerable通用接口。的的   实现在运行时提供给数组,因此是   文档构建工具不可见。结果,通用   接口不会出现在Array的声明语法中   类,并没有接口成员的参考主题   只能通过将数组转换为通用接口类型来访问   (显式接口实现)。

答案 1 :(得分:5)

32位分析分析(全部以ms为单位,仅有趣的位,JIT内联禁用):

Name    Count   'Inc Time'  'Ex Time'   'Avg Inc Time'  'Avg Ex Time'
System.Linq.Enumerable::Count(<UNKNOWN>):int32 <System.Int32>   
        20000000    13338.38    7830.49 0.0007  0.0004
System.SZArrayHelper::get_Count():int32 <System.Int32>  
        10000000    4063.9      2651.44 0.0004  0.0003
System.Collections.Generic.List<System.Int32>::get_Count():int32    
        10000000    1443.99     1443.99 0.0001  0.0001
System.Runtime.CompilerServices.JitHelpers::UnsafeCast(Object):System.__Canon <System.__Canon>  
        10000004    1412.46     1412.46 0.0001  0.0001

System.SZArrayHelper::get_Count()似乎为数组案例调用了System.Runtime.CompilerServices.JitHelpers::UnsafeCast

对于列表,List<int>.Count只返回大小。

Inc time费用包括儿童电话费。 Ex time仅为方法主体的费用。

禁用内联时,Array.Count()慢两倍。

这可能是因为提到了现在已删除的答案。看来应用的属性(ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)SecuritySafeCritical)会阻止运行时内联调用,因此差异很大(在32位模式下,我的情况要慢38倍)。

自己介绍一下:

获取https://github.com/leppie/IronScheme/raw/master/IronScheme/tools/IronScheme.Profiler.x86.dll 运行应用程序(仅限x86版本):

regsvr32 IronScheme.Profiler.x86.dll
set COR_PROFILER={9E2B38F2-7355-4C61-A54F-434B7AC266C0}
set COR_ENABLE_PROFILING=1
ConsoleApp1.exe

当应用退出时,会创建一个report.tab文件,然后可以在Excel中使用。

答案 2 :(得分:3)

我发布这个,不是作为答案,而是提供一个更可测试的环境。

我已经获取了Enumerable<T>.Count()的实际实现的副本并更改了原始测试程序以使用它,因此人们可以在调试器中单步执行它。

如果您运行以下代码的发布版本,您将获得与OP类似的时间。

对于List<T>int[],分配给is2的第一个广告素材将为非空,因此将调用is2.Count

所以看来差异来自于.Count的内部实施。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        public const long Iterations = (long)1e8;

        static void Main()
        {
            var list = new List<int>() { 1 };
            var array = new int[1];
            array[0] = 1;

            var results = new Dictionary<string, TimeSpan>();
            results.Add("int[]", Benchmark(array, Iterations));
            results.Add("List<int>", Benchmark(list, Iterations));

            Console.WriteLine("Function".PadRight(30) + "Count()");
            foreach (var result in results)
            {
                Console.WriteLine("{0}{1}", result.Key.PadRight(30), Math.Round(result.Value.TotalSeconds, 3));
            }
            Console.ReadLine();
        }

        public static TimeSpan Benchmark(IEnumerable<int> source, long iterations)
        {
            var countWatch = new Stopwatch();
            countWatch.Start();
            for (long i = 0; i < iterations; i++) Count(source);
            countWatch.Stop();

            return countWatch.Elapsed;
        }

        public static int Count<TSource>(IEnumerable<TSource> source)
        {
            ICollection<TSource> is2 = source as ICollection<TSource>;

            if (is2 != null)
                return is2.Count;  // This is executed for int[] AND List<int>.

            ICollection is3 = source as ICollection;

            if (is3 != null)
                return is3.Count;

            int num = 0;

            using (IEnumerator<TSource> enumerator = source.GetEnumerator())
            {
                while (enumerator.MoveNext())
                    num++;
            }

            return num;
        }
    }
}

根据这些信息,我们可以简化测试,只关注List.CountArray.Count之间的时间差异:

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            int dummy = 0;
            int count = 1000000000;

            var array = new int[1] as ICollection<int>;
            var list = new List<int> {0};

            var sw = Stopwatch.StartNew();

            for (int i = 0; i < count; ++i)
                dummy += array.Count;

            Console.WriteLine("Array elapsed = " + sw.Elapsed);

            dummy = 0;
            sw.Restart();

            for (int i = 0; i < count; ++i)
                dummy += list.Count;

            Console.WriteLine("List elapsed = " + sw.Elapsed);

            Console.ReadKey(true);
        }
    }
}

上面的代码为调试器外部的发布版本运行提供了以下结果:

Array elapsed = 00:00:02.9586515
List elapsed = 00:00:00.6098578

此时,我自己“当然可以优化Count()以识别T[]并直接返回.Length。所以我将Count()的实施更改为如下:

public static int Count<TSource>(IEnumerable<TSource> source)
{
    var array = source as TSource[];

    if (array != null)        // Optimised for arrays.
        return array.Length;  // This is executed for int[] 

    ICollection<TSource> is2 = source as ICollection<TSource>;

    if (is2 != null)
        return is2.Count;  // This is executed for List<int>.

    ICollection is3 = source as ICollection;

    if (is3 != null)
        return is3.Count;

    int num = 0;

    using (IEnumerator<TSource> enumerator = source.GetEnumerator())
    {
        while (enumerator.MoveNext())
            num++;
    }

    return num;
}

值得注意的是,即使进行了此更改,我的系统上的数组仍然更慢,尽管非数组必须进行额外的转换!

我的结果(发布版本)是:

Function                      Count()
List<int>                     1.753
int[]                         2.304

我完全无法解释最后的结果......

答案 3 :(得分:1)

这是因为int[]需要投射,而List<int>则不需要投射。如果你要使用Length属性,那么结果将会大不相同 - 大约。比List<int>.Count()快10倍。