我有一大堆未缩放的浮点数 - 数组长度为40,000,000。为了扩展这个数组,我认为使用Parallel.For()
会更高效。以下是缩放数据的for循环的顺序版本:
for (i = 0; i < rawData.Length; i++)
{
scaledData[i] = rawData[i] * scale + offset;
}
这是转换为使用Parallel.For()
例如:
Parallel.For(0, rawData.Length, i => {
scaledData[i] = rawData[i] * scale + offset;
});
但表现更差!我的猜测,基于观察索引/线程组合,Parallel.For()
正在以导致过多分页的方式访问内存。为了测试这个理论,我尝试使用Parallel.Invoke()
,如此:
Parallel.Invoke(
() => { for (int i = 0; i < 10000000; i++) { dst[i] = src[i] * scale + offset; } },
() => { for (int i = 10000000; i < 20000000; i++) { dst[i] = src[i] * scale + offset; } },
() => { for (int i = 20000000; i < 30000000; i++) { dst[i] = src[i] * scale + offset; } },
() => { for (int i = 30000000; i < 40000000; i++) { dst[i] = src[i] * scale + offset; } },
);
并且表现明显更好,但我讨厌这段代码的硬编码特性。我有4个处理器,这就是为什么有4个动作传递给Invoke()。
有没有办法哄骗Parallel.For()
以不会破坏内存的方式向线程分发索引?
答案 0 :(得分:4)
您可以使用自定义分区程序来获取所需的行为,而无需使用Parallel.Invoke
。 RangPartitioner
是你想要的开始。
var rangePartitioner = Partitioner.Create(0, rawData.Length);
double[] results = new double[rawData.Length];
Parallel.ForEach(rangePartitioner, (range, loopState) =>
{
for (int i = range.Item1; i < range.Item2; i++)
{
scaledData[i] = rawData[i] * scale * offset;
}
});
您可以创建自定义分区程序并重载GetPartition()
method以将块大小调整为满足您需求的内容。
请参阅Custom Partitioners for PLINQ and TPL进行深入讨论。
这会改善数据的位置吗,是的。前提是您的数组包含值类型。在这种情况下,它们将被分配为连续内存块。对于参考类型,情况并非如此。 FWIW我试图改善这样的记忆局部性但没有惊人的改进。我得出的结论是,CLR可能存在大量其他内存访问,这使得很难理解由此产生的内存访问模式。