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 |

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.


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.