Чому це швидше, якщо я поставив додатковий ToArray перед ToLookup?


10

У нас є короткий метод, який розбирає .csv файл для пошуку:

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

І визначення 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] );
    }
}

І ми з’ясували, що якщо додати додатковий ToArray()раніше, ToLookup()як це:

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

Останнє значно швидше. Більш конкретно, при використанні тестового файлу на 1,4 мільйона рядків перший займає 4,3 секунди, а другий займає близько 3 секунд.

Я вважаю, що ToArray()це потребує додаткового часу, тому останній повинен бути трохи повільнішим. Чому насправді це швидше?


Додаткова інформація:

  1. Ми знайшли цю проблему, оскільки існує інший метод, який розбирає той самий файл .csv у різному форматі, і він займає близько 3 секунд, тому ми вважаємо, що цей процес повинен зробити те ж саме за 3 секунди.

  2. Вихідний тип даних є, Dictionary<string, List<DgvItems>>а вихідний код не використовував linq, і результат подібний.


Тестовий клас 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 );
    }
}

Результат:

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

Я зробив ще одну тестову базу за оригінальним кодом. Здається, проблема не в 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;
    }
}

Результат:

| 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
Я дуже підозрюю код тестування / вимірювання. Будь ласка, опублікуйте код, який обчислює час
Ерно

1
Я здогадуюсь, що без цього .ToArray(), виклик .Select( line => new DgvItems( line ) )повертає IEnumerable перед викликом до ToLookup( item => item.StocksID ). А пошук певного елемента гірше за допомогою IEnumerable, ніж Array. Можливо, швидше конвертувати в масив і виконати пошук, ніж використовувати безлічі.
кімбауді

2
Побічна примітка: поставте var file = File.ReadLines( fileName );- ReadLinesзамість цього ReadAllLinesі ви, мабуть, будете швидше
Дмитро Биченко

2
Ви повинні використовувати BenchmarkDotnetдля фактичного вимірювання перф. Крім того, спробуйте ізолювати фактичний код, який ви хочете виміряти, і не включайте IO в тест.
JohanP

1
Я не знаю, чому це спричинило негативний вплив - я думаю, що це гарне питання.
Руфус Л

Відповіді:


2

Мені вдалося повторити проблему за допомогою спрощеного коду нижче:

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

Важливо, щоб членами створеного кортежу були струни. Видалення двох .ToString()із зазначеного вище коду виключає перевагу ToArray. .NET Framework поводиться дещо інакше, ніж .NET Core, оскільки .ToString()для усунення спостережуваної різниці досить видалити лише перше .

Я поняття не маю, чому це відбувається.


З якою рамкою ви це підтвердили? Я не можу побачити різницю за допомогою .net Framework 4.7.2
Magnus

@Magnus .NET Framework 4.8 (VS 2019, випуск версії)
Theodor Zoulias

Спочатку я перебільшував спостережувану різницю. Він становить близько 20% в .NET Core і близько 10% в .NET Framework.
Теодор Зуліяс

1
Приємний докір. Я не маю конкретних знань, чому це відбувається, і не встигаю це розібратися, але я здогадуюсь , що ToArrayабо ToListзмушує дані знаходитись у суміжній пам'яті; виконання цього примушування на певному етапі конвеєра, навіть якщо це збільшує вартість, може призвести до меншої кількості недоліків кеш-процесора в подальшій операції; Пропуски кеш-процесора напрочуд дорогі.
Ерік Ліпперт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.