簡體   English   中英

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

[英]Array.Count() much slower than List.Count()

當使用IEnumerable<T> Count()的擴展方法時,數組至少比列表慢兩倍。

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

差異來自哪里?

我知道兩者都在調用ICollectionCount屬性:

如果源的類型實現ICollection,則該實現用於獲取元素的數量。 否則,此方法確定計數。

對於列表,它返回List<T>.Count ,對於array, 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的答案非常驚人,但我想補充一句話。
正如Jon Skeet所說:

實際上有兩個等效的塊,只是測試不同的集合接口類型,並使用它首先找到的任何一個(如果有的話)。 我不知道.NET實現是否首先測試ICollection或ICollection <T> - 我可以通過實現兩個接口來測試它,但當然可以從每個接口返回不同的計數,但這可能是過度的。 除了輕微的性能差異之外,對於性能良好的集合並不重要 - 我們希望首先測試“最可能”的接口,我認為這是通用接口。

通用的可能是最有可能發生的,但如果你反轉這兩個,即在通用的之前調用非泛型強制轉換,Array.Count()變得比List.Count()快一點。 另一方面,List的非通用版本較慢。

很高興知道是否有人想在1e8迭代循環中調用Count()

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

原因是Enumerable.Count<T>()執行ICollection<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類的聲明語法中,並且沒有可通過將數組轉換為通用接口類型(顯式接口實現)來訪問的接口成員的參考主題。

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 build):

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

當應用程序退出時,會創建一個report.tab文件,然后可以在Excel中使用該文件。

我發布這個,不是作為答案,而是提供一個更可測試的環境。

我已經獲取了Enumerable<T>.Count()的實際實現的副本,並更改​​了原始測試程序以使用它,因此人們可以在調試器中單步執行它。

如果您運行以下代碼的發布版本,您將獲得與OP類似的時間。

對於List<T>int[] ,分配給is2的第一個is2將為非null,因此將調用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

我完全無法解釋這最后的結果......

這是因為int[]需要強制轉換,而List<int>則不需要。 如果你要使用Length屬性,那么結果將會大不相同 - 大約。 List<int>.Count()快10倍。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM