Dlaczego jest to szybsze, jeśli dodam dodatkowy ToArray przed ToLookup?

10

Mamy krótką metodę, która analizuje plik .csv do odnośnika:

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

I definicja 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] );
    }
}

I stwierdziliśmy, że jeśli dodamy ToArray()wcześniej ToLookup()taki dodatek :

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

Ten ostatni jest znacznie szybszy. Mówiąc dokładniej, gdy używasz pliku testowego zawierającego 1,4 miliona linii, pierwszy zajmuje około 4,3 sekundy, a drugi około 3 sekund.

Spodziewam się, że ToArray()powinno to zająć więcej czasu, więc ta ostatnia powinna być nieco wolniejsza. Dlaczego faktycznie jest szybszy?


Dodatkowe informacje:

  1. Znaleźliśmy ten problem, ponieważ istnieje inna metoda, która analizuje ten sam plik .csv do innego formatu i zajmuje to około 3 sekund, więc uważamy, że ten powinien być w stanie zrobić to samo w ciągu 3 sekund.

  2. Oryginalny typ danych jest, Dictionary<string, List<DgvItems>>a oryginalny kod nie używał linq, a wynik jest podobny.


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

Wynik:

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

Zrobiłem kolejną bazę testową na oryginalnym kodzie. Wydaje się, że problem nie dotyczy 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;
    }
}

Wynik:

| Method |    Mean |    Error |   StdDev |
|------- |--------:|---------:|---------:|
|  First | 2.470 s | 0.0218 s | 0.0182 s |
| Second | 3.481 s | 0.0260 s | 0.0231 s |
Leisen Chang
źródło
2
Bardzo podejrzewam kod testowy / pomiarowy. Proszę zamieścić kod, który oblicza czas
Erno
1
Domyślam się, że bez .ToArray()wywołania .Select( line => new DgvItems( line ) )zwraca IEnumerable przed wywołaniem do ToLookup( item => item.StocksID ). A wyszukiwanie konkretnego elementu jest gorsze przy użyciu IEnumerable niż Array. Prawdopodobnie szybciej przekonwertować na tablicę i wykonać wyszukiwanie niż przy użyciu niepoliczalnego.
kimbaudi
2
Uwaga dodatkowa: wstaw var file = File.ReadLines( fileName );- ReadLineszamiast ReadAllLinesi prawdopodobnie kod będzie szybszy
Dmitry Bychenko
2
Należy użyć BenchmarkDotnetdo rzeczywistego pomiaru perf. Spróbuj także wyizolować rzeczywisty kod, który chcesz zmierzyć, i nie uwzględniaj we / wy testu.
JohanP
1
Nie wiem, dlaczego ma to negatywne zdanie - myślę, że to dobre pytanie.
Rufus L

Odpowiedzi:

2

Udało mi się odtworzyć problem z poniższym uproszczonym kodem:

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

Ważne jest, aby członkowie utworzonej krotki były łańcuchami. Usunięcie dwóch .ToString()z powyższego kodu eliminuje zaletę ToArray. .NET Framework zachowuje się nieco inaczej niż .NET Core, ponieważ wystarczy usunąć tylko pierwszy, .ToString()aby wyeliminować zaobserwowaną różnicę.

Nie mam pojęcia, dlaczego tak się dzieje.

Theodor Zoulias
źródło
Z jakimi ramami to potwierdziłeś? Nie widzę żadnej różnicy przy użyciu frameworku .net 4.7.2
Magnus
@Magnus .NET Framework 4.8 (VS 2019, wydanie kompilacji)
Theodor Zoulias
Początkowo przesadziłem zaobserwowaną różnicę. Jest około 20% w .NET Core i około 10% w .NET Framework.
Theodor Zoulias
1
Niezłe repro. Nie mam szczególnej wiedzy na temat tego, dlaczego tak się dzieje i nie mam czasu, aby to rozgryźć, ale domyślam się, że dane ToArraylub ToListwymuszają przechowywanie danych w ciągłej pamięci; wykonanie tego wymuszania na określonym etapie potoku, nawet jeśli powoduje to wzrost kosztów, może spowodować, że późniejsza operacja będzie miała mniej błędów pamięci podręcznej procesora; Błędy w pamięci podręcznej procesora są zaskakująco drogie.
Eric Lippert,