Как многопроходный GroupBy() может быть быстрее, чем один проход?

Я не могу понять, как GroupBy() работает быстрее для многопроходного ResultSelector, чем для однопроходной версии.

Учитывая этот класс:

    public class DummyItem
    {
        public string Category { get; set; }
        public decimal V1 { get; set; }
        public decimal V2 { get; set; }
    }

Я создаю массив с 100000 записей с некоторыми случайными данными, а затем повторяю следующий запрос:

ПОДХОД 1. Несколько проходов для итогов по категориям

var q = randomData.GroupBy(
   x => x.Category,
   (k, l) => new DummyItem
   {
      Category = k,
      V1 = l.Sum(x => x.V1), // Iterate the items for this category
      V2 = l.Sum(x => x.V2), // Iterate them again
    }
);

Кажется, что это двойная обработка внутреннего перечислимого, где он суммирует V1 и V2 для каждой категории.

Поэтому я собрал следующую альтернативу, предполагая, что это обеспечит лучшую производительность за счет расчета итогов по категориям за один проход.

ПОДХОД 2: Один проход для итогов по категориям

var q = randomData.GroupBy(
    x => x.Category, 
    (k, l) => l.Aggregate( // Iterate the inner list once per category
            new decimal[2], 
            (t,d) => 
            {
                t[0] += d.V1;
                t[1] += d.V2;
                return t;
            },
            t => new DummyItem{ Category = k, V1=t[0], V2=t[1] }
    )
);

Довольно типичные результаты:

'Multiple pass': iterations=5 average=2,961 ms each
'Single pass': iterations=5 average=5,146 ms each

Невероятно, но Подход 2 занимает вдвое больше времени, чем Подход 1. Я провел множество тестов, варьирующих количество свойств V *, количество отдельных категорий и другие факторы. Хотя величина разницы в производительности варьируется, Подход 2 всегда значительно медленнее Подхода 1.

Я что-то упустил здесь? Как Подход 1 может быть быстрее, чем Подход 2?

(Я чувствую, что приближается лицевая маска...)


* ОБНОВЛЕНИЕ *

После ответа @Jirka я подумал, что стоило бы удалить GroupBy() с картинки, чтобы посмотреть, будут ли простые агрегаты в большом списке работать так, как ожидалось. Задача состояла в том, чтобы просто вычислить итоги для двух десятичных переменных в одном и том же списке из 100000 случайных строк.

Результаты продолжили сюрпризы:

СУММА: ForEach

decimal t1 = 0M;
decimal t2 = 0M;
foreach(var item in randomData)
{
    t1 += item.V1;
    t2 += item.V2;
}

Базовая линия. Я считаю, что самый быстрый способ получить требуемый результат.

СУММА: Multipass

x = randomData.Sum(x => x.V1);
y = randomData.Sum(x => x.V2);

СУММА: Single pass

var result = randomData.Aggregate(new DummyItem(), (t, x) => 
{ 
     t.V1 += x.V1; 
     t.V2 += x.V2; 
     return t; 
});

Результаты были следующими:

'SUM: ForEach': iterations=10 average=1,793 ms each
'SUM: Multipass': iterations=10 average=2,030 ms each
'SUM: Singlepass': iterations=10 average=5,714 ms each

Удивительно, но это показывает, что проблема не имеет ничего общего с GroupBy. Поведение согласуется с агрегацией данных в целом. Мое предположение о том, что лучше проводить агрегацию данных за один проход, просто неверно (вероятно, похмелье от моих корней БД).

(лицевая сторона лица)

Как отметил @Jirka, встраивание, очевидно, происходит для многопроходного подхода, что означает, что он лишь незначительно медленнее, чем базовая линия ForEach. Моя наивная попытка оптимизировать за один проход, побежала почти в 3 раза медленнее!

Похоже, что при работе со списками в памяти все, что вы хотите сделать с элементами в списке, скорее всего, будет гораздо большим фактором производительности, чем накладные расходы на итерацию.

1 ответ

Решение

Агрегат должен создать в процессе 99,999 записей активации (для вызовов не встроенных методов). Это компенсирует преимущество одного прохода.

Думайте о количестве, сумме, среднем и т. Д. Как о оптимизированных особых случаях того, что может делать агрегат в общем случае.

Другие вопросы по тегам