Por que é mais rápido se eu colocar um ToArray extra antes do ToLookup?

10

Temos um método curto que analisa o arquivo .csv em uma pesquisa:

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 a definição 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] );
    }
}

E descobrimos que, se adicionarmos um extra ToArray()antes ToLookup()desta forma:

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

O último é significativamente mais rápido. Mais especificamente, quando o arquivo de teste é usado com 1,4 milhão de linhas, o primeiro leva cerca de 4,3 segundos e o segundo leva cerca de 3 segundos.

Espero ToArray()levar um tempo extra para que este último seja um pouco mais lento. Por que é realmente mais rápido?


Informação extra:

  1. Encontramos esse problema porque existe outro método que analisa o mesmo arquivo .csv em um formato diferente e leva cerca de 3 segundos. Portanto, achamos que este deve ser capaz de fazer a mesma coisa em 3 segundos.

  2. O tipo de dados original é Dictionary<string, List<DgvItems>>e o código original não usou linq e o resultado é semelhante.


Classe de teste 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 |

Eu fiz outra base de teste no código original. Parece que o problema não está no 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
fonte
2
Eu suspeito muito o código de teste / medição. Por favor, poste o código que calcula o tempo
Erno
11
Meu palpite é que, sem o .ToArray(), a chamada para .Select( line => new DgvItems( line ) )retorna um IEnumerable antes da chamada para ToLookup( item => item.StocksID ). E procurar um elemento específico é pior usando IEnumerable que Array. Provavelmente, é mais rápido converter em uma matriz e executar pesquisa do que usar um ienumerable.
Kimbaudi 21/11/19
2
Nota lateral: colocar var file = File.ReadLines( fileName );- ReadLinesem vez de ReadAllLinese você código irá provavelmente ser mais rápido
Dmitry Bychenko
2
Você deve usar BenchmarkDotnetpara a medição de desempenho real. Além disso, tente isolar o código real que você deseja medir e não inclua IO no teste.
JohanP
11
Não sei por que isso teve um voto negativo - acho que é uma boa pergunta.
Rufus L

Respostas:

2

Consegui replicar o problema com o código simplificado abaixo:

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

É importante que os membros da tupla criada sejam cadeias de caracteres. Remover os dois .ToString()do código acima elimina a vantagem de ToArray. O .NET Framework se comporta um pouco diferente do .NET Core, pois basta remover apenas o primeiro .ToString()para eliminar a diferença observada.

Eu não tenho idéia do por que isso acontece.

Theodor Zoulias
fonte
Com qual estrutura você confirmou isso? Eu sou incapaz de ver qualquer diferença usando NET Framework 4.7.2
Magnus
@Magnus .NET Framework 4.8 (VS 2019, Versão Build)
Theodor Zoulias
Inicialmente exagerei a diferença observada. É cerca de 20% no .NET Core e cerca de 10% no .NET Framework.
Theodor Zoulias 21/11/19
11
Boa reprodução. Não tenho conhecimento específico de por que isso acontece e não tenho tempo para descobrir, mas meu palpite seria que o ToArrayou ToListforça os dados a estarem na memória contígua; fazer isso forçando em um estágio específico do pipeline, mesmo que adicione custos, pode fazer com que uma operação posterior tenha menos falhas no cache do processador; falhas no cache do processador são surpreendentemente caras.
precisa