ToLookupの前に追加のToArrayを配置すると、なぜ高速になるのですか?


10

.csvファイルを解析してルックアップする短いメソッドがあります。

ILookup<string, DgvItems> ParseCsv( string fileName )
{
    var file = File.ReadAllLines( fileName );
    return file.Skip( 1 ).Select( line => new DgvItems( line ) ).ToLookup( item => item.StocksID );
}

そして、DgvItemsの定義:

public class DgvItems
{
    public string DealDate { get; }

    public string StocksID { get; }

    public string StockName { get; }

    public string SecBrokerID { get; }

    public string SecBrokerName { get; }

    public double Price { get; }

    public int BuyQty { get; }

    public int CellQty { get; }

    public DgvItems( string line )
    {
        var split = line.Split( ',' );
        DealDate = split[0];
        StocksID = split[1];
        StockName = split[2];
        SecBrokerID = split[3];
        SecBrokerName = split[4];
        Price = double.Parse( split[5] );
        BuyQty = int.Parse( split[6] );
        CellQty = int.Parse( split[7] );
    }
}

そして、ToArray()前に次のToLookup()ように追加すると、

static ILookup<string, DgvItems> ParseCsv( string fileName )
{
    var file = File.ReadAllLines( fileName  );
    return file.Skip( 1 ).Select( line => new DgvItems( line ) ).ToArray().ToLookup( item => item.StocksID );
}

後者は大幅に高速です。具体的には、140万行のテストファイルを使用すると、前者は約4.3秒、後者は約3秒かかります。

ToArray()少し時間がかかるはずなので、もっと時間がかかると思います。なぜ実際に速いのですか?


追加情報:

  1. 同じ.csvファイルを別の形式に解析する別の方法があり、約3秒かかるため、この問題は3秒で同じことができるはずだと考えました。

  2. 元のデータ型はでDictionary<string, List<DgvItems>>あり、元のコードはlinqを使用していないため、結果は類似しています。


BenchmarkDotNetテストクラス:

public class TestClass
{
    private readonly string[] Lines;

    public TestClass()
    {
        Lines = File.ReadAllLines( @"D:\20110315_Random.csv" );
    }

    [Benchmark]
    public ILookup<string, DgvItems> First()
    {
        return Lines.Skip( 1 ).Select( line => new DgvItems( line ) ).ToArray().ToLookup( item => item.StocksID );
    }

    [Benchmark]
    public ILookup<string, DgvItems> Second()
    {
        return Lines.Skip( 1 ).Select( line => new DgvItems( line ) ).ToLookup( item => item.StocksID );
    }
}

結果:

| Method |    Mean |    Error |   StdDev |
|------- |--------:|---------:|---------:|
|  First | 2.530 s | 0.0190 s | 0.0178 s |
| Second | 3.620 s | 0.0217 s | 0.0203 s |

元のコードに基づいて別のテストベースを実行しました。問題はLinqではないようです。

public class TestClass
{
    private readonly string[] Lines;

    public TestClass()
    {
        Lines = File.ReadAllLines( @"D:\20110315_Random.csv" );
    }

    [Benchmark]
    public Dictionary<string, List<DgvItems>> First()
    {
        List<DgvItems> itemList = new List<DgvItems>();
        for ( int i = 1; i < Lines.Length; i++ )
        {
            itemList.Add( new DgvItems( Lines[i] ) );
        }

        Dictionary<string, List<DgvItems>> dictionary = new Dictionary<string, List<DgvItems>>();

        foreach( var item in itemList )
        {
            if( dictionary.TryGetValue( item.StocksID, out var list ) )
            {
                list.Add( item );
            }
            else
            {
                dictionary.Add( item.StocksID, new List<DgvItems>() { item } );
            }
        }

        return dictionary;
    }

    [Benchmark]
    public Dictionary<string, List<DgvItems>> Second()
    {
        Dictionary<string, List<DgvItems>> dictionary = new Dictionary<string, List<DgvItems>>();
        for ( int i = 1; i < Lines.Length; i++ )
        {
            var item = new DgvItems( Lines[i] );

            if ( dictionary.TryGetValue( item.StocksID, out var list ) )
            {
                list.Add( item );
            }
            else
            {
                dictionary.Add( item.StocksID, new List<DgvItems>() { item } );
            }
        }

        return dictionary;
    }
}

結果:

| Method |    Mean |    Error |   StdDev |
|------- |--------:|---------:|---------:|
|  First | 2.470 s | 0.0218 s | 0.0182 s |
| Second | 3.481 s | 0.0260 s | 0.0231 s |

2
私はテストコード/測定を非常に疑っています。時間を計算するコードを投稿してください
Erno

1
私の推測では、がない場合.ToArray()、への呼び出しはへ.Select( line => new DgvItems( line ) )の呼び出しの前にIEnumerable を返しますToLookup( item => item.StocksID )。また、特定の要素を検索することは、IEnumerableをArrayを使用するよりも悪くします。おそらく、ienumerableを使用するよりも、配列に変換してルックアップを実行する方が高速です。
kimbaudi

2
サイドノート:置くvar file = File.ReadLines( fileName );- ReadLines代わりにReadAllLines、コードはおそらくより速くなるでしょう
Dmitry Bychenko

2
BenchmarkDotnet実際のパフォーマンス測定に使用する必要があります。また、測定したい実際のコードをテストして分離し、IOをテストに含めないでください。
JohanP

1
なぜこれが反対票を投じたのかわかりません-いい質問だと思います。
Rufus L

回答:


2

以下の簡単なコードで問題を再現できました:

var lookup = Enumerable.Range(0, 2_000_000)
    .Select(i => ( (i % 1000).ToString(), i.ToString() ))
    .ToArray() // +20% speed boost
    .ToLookup(x => x.Item1);

作成されたタプルのメンバーは文字列であることが重要です。.ToString()上記のコードから2つを削除すると、の利点がなくなりToArrayます。.NET Frameworkの動作は.NET Coreとは少し異なります。なぜなら.ToString()、観察された違いを排除するには、最初のものだけを削除するだけで十分だからです。

なぜこれが起こるのか私にはわかりません。


どのフレームワークでこれを確認しましたか?.netフレームワーク4.7.2を使用して違いを確認できません
Magnus

@Magnus .NET Framework 4.8(VS 2019、リリースビルド)
Theodor Zoulias

最初、私は観察された違いを誇張しました。.NET Coreでは約20%、. NET Frameworkでは約10%です。
Theodor Zoulias

1
素敵な再現。なぜこれが発生するのか私には特定の知識がなく、それを理解する時間はありませんが、またはがデータを連続したメモリに置くことを強制していると思います。パイプラインの特定の段階で強制的に実行すると、たとえコストが追加されても、後の操作でプロセッサキャッシュミスが少なくなる可能性があります。プロセッサキャッシュミスは驚くほど高価です。ToArrayToList
Eric Lippert
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.