25

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

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

差异从何而来?

我知道两者都在调用以下Count属性ICollection

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

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

基准:

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;
    }
}

编辑:

leppieKnaģis的答案非常惊人,但我想补充一点。
正如乔恩斯基特所说:

实际上有两个等效的块,只是测试不同的集合接口类型,并使用它首先找到的任何一个(如果有的话)。我不知道 .NET 实现是先测试 ICollection 还是 ICollection< T > - 当然,我可以通过实现两个接口但从每个接口返回不同的计数来测试它,但这可能是矫枉过正。除了轻微的性能差异之外,表现良好的集合并不重要 - 我们想首先测试“最有可能”的接口,我相信它是通用接口。

通用的可能是最有可能发生的,但是如果你颠倒两者,即在通用之前调用非通用转换,Array.Count() 会比 List.Count() 快一点。另一方面,非通用版本的 List 速度较慢。

很高兴知道是否有人想调用Count()1e8 迭代循环!

Function       ICollection<T> Cast     ICollection Cast
List                1,268                   1,738         
Array               5,925                   1,683
4

4 回答 4

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

关键可能是文档中的这个声明(强调我的):

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

于 2013-04-25T08:05:58.167 回答
5

32 位分析分析(全部以毫秒为单位,只有有趣的位,禁用 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 中使用该文件。

于 2013-04-25T08:20:32.043 回答
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.Count有了这些信息,我们可以简化测试,只关注和之间的时间差异Array.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

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

于 2013-04-25T08:24:27.847 回答
1

那是因为int[]需要强制转换,而不需要List<int>。如果您要使用 Length 属性,那么结果将完全不同 - 大约。比 .快 10 倍List<int>.Count()

于 2013-04-25T08:02:31.813 回答