【问题标题】:Array.Count() much slower than List.Count()Array.Count() 比 List.Count() 慢得多
【发布时间】:2013-04-25 07:37:27
【问题描述】:

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

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

差异从何而来?

我知道两者都在调用ICollectionCount 属性:

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

对于列表,它返回List&lt;T&gt;.Count,对于数组,Array.Length。此外,Array.Length 应该比List&lt;T&gt;.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 的答案非常棒,但我想补充一句。
As Jon Skeet said:

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

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

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

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

【问题讨论】:

  • +1 有趣。此外,根据您的数字,您似乎正在运行 64 位。在 32 位中,差别更大!
  • 我的测试实际上慢了 四倍!最有趣的。
  • 在 32 位中,对我来说慢了大约 38 倍 O_o

标签: c# .net performance linq ienumerable


【解决方案1】:

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

使用此示例代码:

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 通用接口。 该 实现在运行时提供给数组,因此是 对文档构建工具不可见。结果,通用 接口没有出现在数组的声明语法中 类,并且没有针对接口成员的参考主题 只能通过将数组转换为通用接口类型来访问 (显式接口实现)。

【讨论】:

  • 抱歉,这不是真的。 int[] 绝对DOES 实现ICollection&lt;int&gt; 我已经单步执行Enumerable&lt;T&gt;.Count() 并验证它没有进行两次强制转换。
  • 是的,int[] 确实实现了ICollection&lt;T&gt;,但演员本身很慢...会修改答案...
  • @MatthewWatson:我想我们都得出结论,在数组的情况下,转换为 ICollection&lt;int&gt; 是特殊处理的。
  • 我们可以得出结论,ICollection 是在运行时通过做一些疯狂的事情来检索而不是简单的接口实例转换。
  • @leppie 我的评论是针对这个答案的原始形式;它并不真正适用于最新版本。
【解决方案2】:

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&lt;int&gt;.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 中使用该文件。

【讨论】:

  • 做了一个测试,var genericCollection = source as ICollection&lt;TSource&gt;; 使用数组比使用列表慢五倍。有趣的是,对 ICollection 的强制转换要快得多!
  • @Scorpi0:有趣 :) 这可能会导致 JitHelpers::UnsafeCast 调用。
  • 属性似乎不是原因,因为当直接调用 List&lt;T&gt;.CountArray.Length 时(中间没有 Enumerable),它们的性能完全相同......
  • @Knaģis:没有分析器,它会产生巨大的差异。使用分析器(禁用 JIT 内联),差异只有 2 倍。
  • @leppie 关于为什么这个演员选择这条路的任何线索,还是我必须发布另一个问题?
【解决方案3】:

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

我把Enumerable&lt;T&gt;.Count()的实际实现复制了一份,把原来的测试程序改成使用它,这样大家就可以在调试器中单步执行了。

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

对于List&lt;T&gt;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

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

【讨论】:

  • 出乎意料的是arr as int[]arr as ICollection慢!
  • @Scorpi0 确实,我想知道我是否一定犯了某种错误,但我看不出它是否有一个......
  • 实际上,特殊的外壳是数组减速的一部分。通常类型检查非常便宜。但不幸的是,CLR 支持数组协方差,所以首先它会检查它是否是一个数组(便宜),然后它会进行类型检查以查看数组转换是否安全(昂贵)。即使它应该是一个 noop 它不是。
【解决方案4】:

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

【讨论】:

    猜你喜欢
    • 2010-11-02
    • 2011-10-27
    • 2013-08-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-12
    相关资源
    最近更新 更多