Pourquoi est-ce plus rapide si je mets un ToArray supplémentaire avant ToLookup?

10

Nous avons une méthode courte qui analyse le fichier .csv en recherche:

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

Et la définition 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] );
    }
}

Et nous avons constaté que si nous ajoutons un supplément ToArray()avant ToLookup()comme ceci:

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

Ce dernier est nettement plus rapide. Plus précisément, lorsque vous utilisez un fichier de test avec 1,4 million de lignes, le premier prend environ 4,3 secondes et le dernier prend environ 3 secondes.

Je pense que cela ToArray()devrait prendre plus de temps, donc ce dernier devrait être légèrement plus lent. Pourquoi est-ce réellement plus rapide?


Informations supplémentaires:

  1. Nous avons trouvé ce problème car il existe une autre méthode qui analyse le même fichier .csv dans un format différent et cela prend environ 3 secondes, nous pensons donc que celui-ci devrait être capable de faire la même chose en 3 secondes.

  2. Le type de données d'origine est Dictionary<string, List<DgvItems>>et le code d'origine n'a pas utilisé linq et le résultat est similaire.


Classe de test 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 );
    }
}

Résultat:

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

J'ai fait une autre base de test sur le code original. Semble que le problème n'est pas sur 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;
    }
}

Résultat:

| 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
la source
2
Je soupçonne fortement le code de test / la mesure. Veuillez poster le code qui calcule l'heure
Erno
1
Je suppose que sans le .ToArray(), l'appel à .Select( line => new DgvItems( line ) )renvoie un IEnumerable avant l'appel à ToLookup( item => item.StocksID ). Et rechercher un élément particulier est pire en utilisant IEnumerable que Array. Probablement plus rapide pour convertir en tableau et effectuer une recherche que d'utiliser un ienumerable.
kimbaudi
2
Note latérale: mettez var file = File.ReadLines( fileName );- ReadLinesau lieu de ReadAllLineset votre code sera probablement plus rapide
Dmitry Bychenko
2
Vous devez utiliser BenchmarkDotnetpour la mesure réelle de la perf. Essayez également d'isoler le code réel que vous souhaitez mesurer et n'incluez pas d'E / S dans le test.
JohanP
1
Je ne sais pas pourquoi cela a obtenu un downvote - je pense que c'est une bonne question.
Rufus L

Réponses:

2

J'ai réussi à reproduire le problème avec le code simplifié ci-dessous:

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

Il est important que les membres du tuple créé soient des chaînes. Supprimer les deux.ToString() du code ci-dessus élimine l'avantage de ToArray. Le .NET Framework se comporte un peu différemment du .NET Core, car il suffit de supprimer uniquement le premier .ToString()pour éliminer la différence observée.

Je ne sais pas pourquoi cela se produit.

Theodor Zoulias
la source
Avec quel cadre avez-vous confirmé cela? Je ne vois aucune différence en utilisant le framework .net 4.7.2
Magnus
@Magnus .NET Framework 4.8 (VS 2019, Release Build)
Theodor Zoulias
Au début, j'ai exagéré la différence observée. Il est d'environ 20% dans .NET Core et d'environ 10% dans .NET Framework.
Theodor Zoulias
1
Belle repro. Je n'ai aucune connaissance précise de la raison pour laquelle cela se produit et je n'ai pas le temps de le comprendre, mais je suppose que le ToArrayou ToListforce les données à être dans la mémoire contiguë; faire ce forçage à un stade particulier du pipeline, même s'il ajoute du coût, peut entraîner une opération ultérieure pour avoir moins de ratés de cache de processeur; les erreurs de cache du processeur sont étonnamment coûteuses.
Eric Lippert