多通GroupBy()如何比单通更快?

时间:2012-03-14 07:36:36

标签: c# ienumerable linq-to-objects

我无法理解GroupBy()对多传递ResultSelector的执行速度比单传传递更快。

鉴于此课程:

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

我使用一些随机数据创建一个包含100,000个条目的数组,然后迭代以下查询:

方法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()以查看大型列表上的简单聚合是否按预期执行。任务只是计算同一个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:单通道

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所指出的那样,多通道方法显然存在内陷,意味着它只比基线慢了一点,而且每个方法都有。我天真地尝试优化到单程,跑得差不多3倍!

在处理内存列表时,无论你希望对列表中的项目做什么,它的性能都可能远远超过迭代开销。

1 个答案:

答案 0 :(得分:1)

聚合必须在此过程中创建99,999个激活记录(对于非可内联方法调用)。这抵消了单程的优势。

将Count,Sum,Average等视为Aggregate在一般情况下可以做的优化特例。