Mengapa lebih cepat jika saya meletakkan ToArray tambahan sebelum ToLookup?


10

Kami memiliki metode singkat yang mem-parsing file .csv ke pencarian:

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

Dan definisi 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] );
    }
}

Dan kami menemukan bahwa jika kami menambahkan ekstra ToArray()sebelum ToLookup()seperti ini:

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

Yang terakhir ini secara signifikan lebih cepat. Lebih khusus lagi, ketika menggunakan file uji dengan 1,4 juta baris, yang pertama membutuhkan waktu sekitar 4,3 detik dan yang terakhir membutuhkan waktu sekitar 3 detik.

Saya berharap ToArray()harus mengambil waktu ekstra sehingga yang terakhir harus sedikit lebih lambat. Mengapa sebenarnya lebih cepat?


Informasi tambahan:

  1. Kami menemukan masalah ini karena ada metode lain yang mengurai file .csv yang sama ke format yang berbeda dan dibutuhkan sekitar 3 detik sehingga kami pikir yang satu ini harus dapat melakukan hal yang sama dalam 3 detik.

  2. Tipe data asli adalah Dictionary<string, List<DgvItems>>dan kode asli tidak menggunakan LINQ dan hasilnya mirip.


Kelas tes 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 );
    }
}

Hasil:

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

Saya melakukan tes dasar lain pada kode asli. Tampaknya masalahnya bukan pada 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;
    }
}

Hasil:

| 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
Saya sangat mencurigai kode pengujian / pengukuran. Silakan kirim kode yang menghitung waktu
Erno

1
Dugaan saya adalah bahwa tanpa .ToArray(), panggilan untuk .Select( line => new DgvItems( line ) )mengembalikan IEnumerable sebelum panggilan ke ToLookup( item => item.StocksID ). Dan mencari elemen tertentu lebih buruk menggunakan IEnumerable daripada Array. Mungkin lebih cepat untuk mengkonversi ke array dan melakukan pencarian daripada menggunakan ienumerable.
kimbaudi

2
Catatan: put var file = File.ReadLines( fileName );- ReadLinesbukannya ReadAllLinesdan Anda kode mungkin akan lebih cepat
Dmitry Bychenko

2
Anda harus menggunakan BenchmarkDotnetpengukuran perf aktual. Juga, coba dan isolasi kode aktual yang ingin Anda ukur dan tidak menyertakan IO dalam pengujian.
JohanP

1
Saya tidak tahu mengapa ini mendapat downvote - saya pikir itu pertanyaan yang bagus.
Rufus L

Jawaban:


2

Saya berhasil mereplikasi masalah dengan kode sederhana di bawah ini:

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

Penting bahwa anggota tuple yang dibuat adalah string. Menghapus keduanya .ToString()dari kode di atas menghilangkan keuntungan dari ToArray. NET Framework. Berperilaku sedikit berbeda dari .NET Core, karena cukup menghapus hanya yang pertama .ToString()untuk menghilangkan perbedaan yang diamati.

Saya tidak tahu mengapa ini terjadi.


Kerangka mana yang Anda konfirmasi dengan ini? Saya tidak dapat melihat perbedaan menggunakan .net framework 4.7.2
Magnus

@Magnus .NET Framework 4.8 (VS 2019, Release Build)
Theodor Zoulias

Awalnya saya melebih-lebihkan perbedaan yang diamati. Ini sekitar 20% di .NET Core, dan sekitar 10% di .NET Framework.
Theodor Zoulias

1
Repro yang bagus. Saya tidak memiliki pengetahuan khusus tentang mengapa hal ini terjadi dan tidak punya waktu untuk mencari tahu, tetapi dugaan saya adalah bahwa ToArrayatau ToListmemaksa data berada dalam memori yang berdekatan; melakukan pemaksaan pada tahap tertentu dalam pipa, meskipun menambah biaya, dapat menyebabkan operasi di kemudian hari memiliki lebih sedikit cache prosesor yang hilang; cache prosesor sangat mahal.
Eric Lippert
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.