Почему быстрее, если я добавлю дополнительный 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. Вероятно, быстрее преобразовать в массив и выполнить поиск, чем при использовании ienumerable.
Кимбауди
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 .NET Framework 4.8 (VS 2019, Release Build)
Теодор Зулиас,
Изначально я преувеличивал наблюдаемую разницу. Это около 20% в .NET Core и около 10% в .NET Framework.
Теодор Зулиас
1
Хорошее репро. У меня нет конкретных знаний о том, почему это происходит, и у меня нет времени, чтобы это выяснить, но я предполагаю , что данные ToArrayили ToListзаставляют данные находиться в непрерывной памяти; выполнение этого форсирования на определенной стадии конвейера, даже если это увеличивает стоимость, может привести к тому, что в более поздней операции будет меньше пропусков кэша процессора; промахи кеша процессора удивительно дороги.
Эрик Липперт