Perché è più veloce se inserisco un ToArray aggiuntivo prima di ToLookup?


10

Abbiamo un metodo breve che analizza il file .csv in una ricerca:

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

E la definizione di 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] );
    }
}

E abbiamo scoperto che se aggiungiamo un extra ToArray()prima in ToLookup()questo modo:

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

Quest'ultimo è significativamente più veloce. Più specificamente, quando si utilizza un file di test con 1,4 milioni di righe, il primo impiega circa 4,3 secondi e il secondo dura circa 3 secondi.

Mi aspetto ToArray()che occorra tempo extra, quindi quest'ultimo dovrebbe essere leggermente più lento. Perché è effettivamente più veloce?


Ulteriori informazioni:

  1. Abbiamo riscontrato questo problema perché esiste un altro metodo che analizza lo stesso file .csv in un formato diverso e impiega circa 3 secondi, quindi pensiamo che questo dovrebbe essere in grado di fare la stessa cosa in 3 secondi.

  2. Il tipo di dati originale è Dictionary<string, List<DgvItems>>e il codice originale non utilizzava linq e il risultato è simile.


Classe di prova 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 );
    }
}

Risultato:

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

Ho fatto un'altra base di test sul codice originale. Sembra che il problema non sia su 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;
    }
}

Risultato:

| 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
Sospetto fortemente il codice di test / misurazione. Si prega di inserire il codice che calcola il tempo
Erno

1
La mia ipotesi è che senza il .ToArray(), la chiamata a .Select( line => new DgvItems( line ) )restituisce un IEnumerable prima della chiamata a ToLookup( item => item.StocksID ). E cercare un particolare elemento è peggio usando IEnumerable di Array. Probabilmente più veloce per convertire in un array ed eseguire ricerche rispetto all'utilizzo di un ienumerable.
Kimbaudi,

2
Nota a var file = File.ReadLines( fileName );ReadLinesReadAllLines
margine

2
È necessario utilizzare BenchmarkDotnetper la misurazione della perf effettiva. Inoltre, prova a isolare il codice effettivo che desideri misurare e non includere IO nel test.
JohanP,

1
Non so perché questo abbia avuto un downvote - penso che sia una buona domanda.
Rufus L

Risposte:


2

Sono riuscito a replicare il problema con il codice semplificato di seguito:

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

È importante che i membri della tupla creata siano stringhe. Rimuovere i due .ToString()dal codice sopra elimina il vantaggio di ToArray. .NET Framework si comporta in modo leggermente diverso rispetto a .NET Core, poiché è sufficiente rimuovere solo il primo .ToString()per eliminare la differenza osservata.

Non ho idea del perché questo accada.


Con quale framework lo hai confermato? Non riesco a vedere alcuna differenza usando il framework .net 4.7.2
Magnus,

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

Inizialmente ho esagerato la differenza osservata. È circa il 20% in .NET Core e circa il 10% in .NET Framework.
Theodor Zoulias,

1
Bella riproduzione. Non ho una conoscenza specifica del perché ciò accada e non ho tempo di capirlo, ma la mia ipotesi sarebbe che i dati ToArrayo ToListforzino la memoria contigua; farlo forzando in una determinata fase della pipeline, anche se aggiunge costi, può causare a un'operazione successiva un minor numero di mancanze della cache del processore; i mancati errori nella cache del processore sono sorprendentemente costosi.
Eric Lippert,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.