Я не могу понять, как функция GroupBy() работает быстрее для многопроходного ResultSelector, чем для однопроходной версии.Как многопроцессор GroupBy() может быть быстрее, чем один проход?
Учитывая этот класс:
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?
(Я чувствую Facepalm приходит ...)
* UPDATE *
После @ ответ Ирка, я думал, что это будет стоить удаление GroupBy() из чтобы убедиться, что простые агрегации в большом списке выполняются так, как ожидалось. Задача состояла в том, чтобы просто вычислить итоговые значения для двух десятичных переменных в том же списке из 100 000 случайных строк.
Результаты продолжали сюрпризы:
SUM: ForEach
decimal t1 = 0M;
decimal t2 = 0M;
foreach(var item in randomData)
{
t1 += item.V1;
t2 += item.V2;
}
Базовый. Я считаю, что самый быстрый способ получить требуемый результат.
SUM: Multipass
x = randomData.Sum(x => x.V1);
y = randomData.Sum(x => x.V2);
SUM: SinglePass
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. Поведение в целом согласуется с агрегацией данных. Мое предположение, что лучше всего выполнять агрегацию данных за один проход, просто неверно (возможно, похмелье из моих корней db).
(Facepalm)
Как @Jirka указал на в-подкладку, по-видимому происходя для многопроходной подхода, означает, что он лишь немного медленнее, чем исходно «ForEach». Моя наивная попытка оптимизировать до одного прохода, работала почти в 3 раза медленнее!
Похоже, что при работе с списками в памяти, что бы вы ни делали с элементами в списке, скорее всего, будет гораздо большим фактором производительности, чем накладные расходы итерации.
Спасибо, что поделились своими дополнительными наблюдениями. Не выбрасывайте свою интуицию. Алгоритмы с одним проходом имеют преимущество в производительности для данных более 1 МБ. Но здесь это преимущество было затмевано вызовами методов, происходящими в самом внутреннем (узком) цикле. –