C# 多通道GroupBy()如何比单通道更快?
我无法理解GroupBy()在多过程ResultSelector中的执行速度如何比在单过程版本中更快 鉴于这一类别:C# 多通道GroupBy()如何比单通道更快?,c#,ienumerable,linq-to-objects,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; } } 我创建了一个包含100000个条目和一些随机数据的数
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
}
);
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] }
)
);
x = randomData.Sum(x => x.V1);
y = randomData.Sum(x => x.V2);
var result = randomData.Aggregate(new DummyItem(), (t, x) =>
{
t.V1 += x.V1;
t.V2 += x.V2;
return t;
});
它似乎在双重处理内部枚举,其中对每个类别的V1和V2求和
因此,我将以下备选方案放在一起,假设这将通过在一次传递中计算类别总数来提供更好的性能
方法2:类别合计单次通过
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
}
);
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] }
)
);
x = randomData.Sum(x => x.V1);
y = randomData.Sum(x => x.V2);
var result = randomData.Aggregate(new DummyItem(), (t, x) =>
{
t.V1 += x.V1;
t.V2 += x.V2;
return t;
});
相当典型的结果:
'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个随机行的同一列表中两个十进制变量的总数 结果继续令人惊讶: SUM:ForEach
decimal t1 = 0M;
decimal t2 = 0M;
foreach(var item in randomData)
{
t1 += item.V1;
t2 += item.V2;
}
基线。我相信获得所需输出的最快方法
SUM:Multipass
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
}
);
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] }
)
);
x = randomData.Sum(x => x.V1);
y = randomData.Sum(x => x.V2);
var result = randomData.Aggregate(new DummyItem(), (t, x) =>
{
t.V1 += x.V1;
t.V2 += x.V2;
return t;
});
SUM:Singlepass
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
}
);
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] }
)
);
x = randomData.Sum(x => x.V1);
y = randomData.Sum(x => x.V2);
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倍
在处理内存中的列表时,无论您希望对列表中的项执行什么操作,都可能比迭代开销对性能的影响更大。聚合必须在过程中创建99999条激活记录(对于不可内联的方法调用)。这抵消了单程的优势
将计数、总和、平均数等视为聚合在一般情况下可以做什么的优化特例。谢谢@Jirka。否该数组仅分配一次作为要聚合的种子。对于我的一些测试,这只有四次(即只有四个类别)。当迭代每个类别的可枚举项时,数组只会被更新。@degorolls-你说得对,我很抱歉疏忽了。我更正了我的答案。太棒了!谢谢@Jirka。我已经纠正了一个相当基本的误解……可能是使用Sum()更好地使用处理器寄存器、缓存等吗?就像当你计算几个多路径的总数时,它可能是一个处理器操作码,但是当你进行一次单次计算时,你必须添加到一个summ,store,获取其他summ,Add,store?@Alexander-结构DummyItem可能有大约40字节长,这排除了理论上可用于汇总紧密压缩数据的SSE优化(.NET可能无论如何都做不到)。数据可能有点太大,无法放入二级缓存中,这使单次传递算法具有优势。但是,我假设总和计算主要由读取输入控制,而聚合计算则由堆栈操作控制;堆栈操作包括缓存命中,同时输入未命中。感谢分享您的其他观察结果。不要放弃您的直觉。单次传递算法对于超过1 MB的数据确实具有性能优势。但在这里,这种优势与最内部(瓶颈)循环中发生的方法调用相比相形见绌。