2012-03-14 57 views
2

我无法理解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:A类单通道总计

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更快?

(我感觉到捂脸来了...)


* UPDATE *

后@尔卡的答案,我认为这将是值得去除的GroupBy()图片以查看是否按预期方式执行大型列表上的简单聚合。该任务仅仅是计算在100,000个随机行的同一列表上的两个十进制变量的总计。

结果延续了惊喜:

SUM:的ForEach

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

基线。我相信获得所需产出的最快方式。

SUM:多道

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。该行为通常与数据聚合一致。我认为一次完成数据聚合更好的假设是错误的(可能是我的数据库根源导致的宿醉)。

捂脸

正如@Jirka指出了在衬里表面上存在的用于多遍方法中,意味着它是仅比基线“的ForEach”慢。我天真的尝试优化到单通,跑慢了近3倍!

看来,在处理内存中列表时,无论您希望对列表中的项目执行什么操作,都可能是性能上的一个更大的因素,而不是迭代开销。

+0

感谢您分享您的其他意见。不要放弃你的直觉。单程算法确实对大约1 MB的数据具有性能优势。但是,这种优势在最内层(瓶颈)循环中发生的方法调用显得相形见绌。 – 2012-03-14 13:24:32

回答

1

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

将计数,总和,平均值等作为聚合在一般情况下可以执行的优化特殊情况。

+1

谢谢@Jirka。否该数组仅被分配一次作为聚合的种子。对于我的一些测试,这只有四次(即只有四个类别)。迭代每个类别的枚举时,数组只是更新。 – 2012-03-14 09:33:55

+1

@degorolls - 你是对的,我很抱歉的疏忽。我纠正了我的答案。 – 2012-03-14 10:26:52

+0

迷人!谢谢@Jirka。我有一个相当根本的误解更正... – 2012-03-14 11:22:48