¿Por qué es más rápido si pongo un ToArray adicional antes de ToLookup?

10

Tenemos un método corto que analiza el archivo .csv a una búsqueda:

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

Y la definición de 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] );
    }
}

Y descubrimos que si agregamos un extra ToArray()antes ToLookup()como este:

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

Este último es significativamente más rápido. Más específicamente, cuando se usa un archivo de prueba con 1,4 millones de líneas, el primero toma alrededor de 4,3 segundos y el segundo toma alrededor de 3 segundos.

Espero que ToArray()tome más tiempo, por lo que este último debería ser un poco más lento. ¿Por qué es realmente más rápido?


Información extra:

  1. Encontramos este problema porque hay otro método que analiza el mismo archivo .csv a un formato diferente y toma alrededor de 3 segundos, por lo que creemos que este debería ser capaz de hacer lo mismo en 3 segundos.

  2. El tipo de datos original es Dictionary<string, List<DgvItems>>y el código original no utilizó linq y el resultado es similar.


Clase de prueba 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 );
    }
}

Resultado:

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

Hice otra base de prueba en el código original. Parece que el problema no está en 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;
    }
}

Resultado:

| 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
fuente
2
Sospecho mucho el código de prueba / medición. Por favor enviar el código que calcula el tiempo
Erno
1
Supongo que sin el .ToArray(), la llamada a .Select( line => new DgvItems( line ) )devuelve un IEnumerable antes de la llamada a ToLookup( item => item.StocksID ). Y buscar un elemento en particular es peor usando IEnumerable que Array. Probablemente más rápido para convertir a una matriz y realizar búsquedas que usar un número innumerable.
Kimbaudi
2
Nota al var file = File.ReadLines( fileName );ReadLinesReadAllLines
margen
2
Debe usar BenchmarkDotnetpara medir el rendimiento real. Además, intente aislar el código real que desea medir y no incluya IO en la prueba.
JohanP
1
No sé por qué esto recibió un voto negativo, creo que es una buena pregunta.
Rufus L

Respuestas:

2

Logré replicar el problema con el siguiente código simplificado:

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

Es importante que los miembros de la tupla creada sean cadenas. Eliminar los dos .ToString()del código anterior elimina la ventaja de ToArray. .NET Framework se comporta un poco diferente a .NET Core, ya que es suficiente para eliminar solo el primero .ToString()y eliminar la diferencia observada.

No tengo idea de por qué sucede esto.

Theodor Zoulias
fuente
¿Con qué marco confirmó esto? No puedo ver ninguna diferencia usando .net framework 4.7.2
Magnus
@Magnus .NET Framework 4.8 (VS 2019, Release Build)
Theodor Zoulias
Inicialmente exageré la diferencia observada. Es alrededor del 20% en .NET Core y alrededor del 10% en .NET Framework.
Theodor Zoulias el
1
Buena repro. No tengo conocimiento específico de por qué sucede esto y no tengo tiempo para resolverlo, pero supongo que la ToArrayo ToListobliga a los datos a estar en la memoria contigua; hacer ese forzamiento en una etapa particular de la tubería, a pesar de que agrega costos, puede causar que una operación posterior tenga menos errores de caché del procesador; los errores de caché del procesador son sorprendentemente caros.
Eric Lippert el