关于C#:array.count()比list.count()慢得多

Array.Count() much slower than List.Count()

当使用IEnumerableCount()的扩展方法时,数组至少比列表慢两倍。

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

差异从何而来?

我了解到,双方都称ICollectionCount财产为:

If the type of source implements ICollection, that implementation is used to obtain the count of elements. Otherwise, this method determines the count.

对于列表,返回List.Count,对于数组,返回Array.Length。此外,Array.LengthList.Count快。

基准:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
    }
}

编辑:

莱皮和Kna?答案很神奇,但我想加一句话。正如乔恩·斯基特所说:

There are effectively two equivalent blocks, just testing for
different collection interface types, and using whichever one it finds
first (if any). I don't know whether the .NET implementation tests for
ICollection or ICollection< T > first - I could test it by implementing
both interfaces but returning different counts from each, of course,
but that's probably overkill. It doesn't really matter for
well-behaved collections other than the slight performance difference
- we want to test the"most likely" interface first, which I believe is the generic one.

一般类型的转换最有可能发生,但是如果您反转这两个转换,即在一般类型转换之前调用非一般类型的转换,array.count()会比list.count()快一点。另一方面,非通用版本的列表速度较慢。

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

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


原因是Enumerable.Count()执行强制转换到ICollection以从列表和数组中检索计数。

使用此示例代码:

1
2
3
4
5
6
7
8
public static int Count<TSource>(IEnumerable<TSource> source)
{
    ICollection<TSource> collection = source as ICollection<TSource>;
    if (collection != null)
    {
        return 1; // collection.Count;
    }
}

您可以确定数组的强制转换花费的时间要长得多,事实上,计算所花费的大部分时间都来自于此强制转换:

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

关键可能是文档中的这个语句(重点是我的):

In the .NET Framework version 2.0, the Array class implements the System.Collections.Generic.IList,
System.Collections.Generic.ICollection, and
System.Collections.Generic.IEnumerable generic interfaces. The
implementations are provided to arrays at run time, and therefore are
not visible to the documentation build tools. As a result, the generic
interfaces do not appear in the declaration syntax for the Array
class, and there are no reference topics for interface members that
are accessible only by casting an array to the generic interface type
(explicit interface implementations).


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

1
2
3
4
5
6
7
8
9
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.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版本)运行为:

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

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


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

我已经复制了Enumerable.Count()的实际实现,并将原始测试程序更改为使用它,这样人们就可以在调试器中单步执行它。

如果您运行下面的代码的发布版本,您将得到与OP类似的时间安排。

对于Listint[]两种情况,分配给is2的第一个广播将不为空,因此将调用is2.Count

因此,似乎差异来自于.Count的内部实施。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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之间的时间差:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
        }
    }
}

上面的代码给出了在调试器外部运行的版本生成的以下结果:

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

在这一点上,我心里想,"我们当然可以优化Count()来识别T[],直接返回.Length"。因此,我对Count()的实施作了如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
}

值得注意的是,即使进行了这种更改,阵列在我的系统上仍然较慢,尽管非阵列必须进行额外的转换!

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

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

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


这是因为int[]需要铸造,而List不需要铸造。如果您要使用长度属性,那么结果将非常不同-比List.Count()快大约10倍。