有没有办法在使用Parallel.For时控制空间/内存局部性

时间:2014-04-24 02:21:59

标签: c# multithreading parallel-processing task-parallel-library

我有一大堆未缩放的浮点数 - 数组长度为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()以不会破坏内存的方式向线程分发索引?

1 个答案:

答案 0 :(得分:4)

您可以使用自定义分区程序来获取所需的行为,而无需使用Parallel.InvokeRangPartitioner是你想要的开始。

    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可能存在大量其他内存访问,这使得很难理解由此产生的内存访问模式。