使用Parallel.For令人失望的性能

时间:2012-06-01 07:59:31

标签: c# performance task-parallel-library

我正在尝试使用Parallel.For加快计算时间。我有一个带有8个内核的英特尔酷睿i7 Q840 CPU,但与序列for循环相比,我只能获得4的性能比。这是否与Parallel.For一样好,或者可以调整方法调用以提高性能?

这是我的测试代码,顺序:

var loops = 200;
var perloop = 10000000;

var sum = 0.0;
for (var k = 0; k < loops; ++k)
{
    var sumk = 0.0;
    for (var i = 0; i < perloop; ++i) sumk += (1.0 / i) * i;
    sum += sumk;
}

并行:

sum = 0.0;
Parallel.For(0, loops,
                k =>
                    {
                        var sumk = 0.0;
                        for (var i = 0; i < perloop; ++i) sumk += (1.0 / i) * i;
                        sum += sumk;
                    });

我并行化的循环涉及使用“全局”定义的变量sum进行计算,但这应该只占并行化循环中总时间的一小部分。

在发布版本(“优化代码”标志集)中,顺序for循环在我的计算机上需要33.7秒,而Parallel.For循环需要8.4秒,性能比仅为4.0

在任务管理器中,我可以看到在顺序计算期间CPU利用率为10-11%,而在并行计算期间仅为70%。我试图明确设置

ParallelOptions.MaxDegreesOfParallelism = Environment.ProcessorCount

但无济于事。我不清楚为什么不将所有CPU功率分配给并行计算?

Sequential vs. parallel CPU utilization

我注意到在SO before上提出了类似的问题,结果更令人失望。但是,该问题还涉及第三方库中较差的并行化。我主要关注的是核心库中基本操作的并行化。

更新

我在一些评论中向我指出,我使用的CPU只有4个物理内核,如果启用超线程,系统可以看到8个内核。为此,我禁用了超线程并重新进行了基准测试。

通过超线程禁用,我的计算现在更快,无论是并行还是(我认为是)顺序for循环。 for循环期间的CPU利用率高达约。 Parallel.For循环期间45%(!!!)和100%。

for循环15.6秒的计算时间(超线程启用的速度快两倍)和Parallel.For的6.2秒(比超线程启用)。与Parallel.For的效果比现在只有 2.5 ,在4个真核上运行。

因此,尽管禁用了超线程,但性能比仍远低于预期。另一方面,有趣的是for循环期间CPU利用率如此之高?在这个循环中是否会出现某种内部并行化?

4 个答案:

答案 0 :(得分:23)

即使您没有使用锁,使用全局变量也会引入严重的同步问题。为变量赋值时,每个核心必须访问系统内存中的相同位置,或者在访问之前等待另一个核心完成。 通过使用较轻的Interlocked.Add方法在操作系统级别以原子方式向值添加值,可以避免没有锁定的损坏,但是由于争用仍然会导致延迟。

执行此操作的正确方法是更新线程局部变量以创建部分和,并将所有部分加到最后的单个全局和中。 Parallel.For有一个重载就是这样。 MSDN甚至在How To: Write a Parallel.For Loop that has Thread Local Variables

使用了sumation
        int[] nums = Enumerable.Range(0, 1000000).ToArray();
        long total = 0;

        // Use type parameter to make subtotal a long, not an int
        Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) =>
        {
            subtotal += nums[j];
            return subtotal;
        },
            (x) => Interlocked.Add(ref total, x)
        );

每个主题更新自己的小计值,并在完成后使用Interlocked.Add更新全局总计

答案 1 :(得分:6)

Parallel.For和Parallel.ForEach将使用它认为合适的一定程度的并行性,平衡设置和拆除线程的成本以及它期望每个线程将执行的工作。 .NET 4.5 made several improvements to performance (including more intelligent decisions on the number of threads to spin up) compared to previous .NET versions.

请注意,即使它是为每个核心启动一个线程,上下文切换,false sharing问题,资源锁定和其他问题可能会阻止您实现线性可伸缩性(通常,不一定与您的特定代码示例)。

答案 2 :(得分:5)

我认为计算收益是如此之低,因为你的代码“太容易”在每次迭代中处理其他任务 - 因为parallel.for只是在每次迭代中创建新任务,所以这需要时间来在线程中为它们提供服务。我会这样:

int[] nums = Enumerable.Range(0, 1000000).ToArray();
long total = 0;

Parallel.ForEach(
    Partitioner.Create(0, nums.Length),
    () => 0,
    (part, loopState, partSum) =>
    {
        for (int i = part.Item1; i < part.Item2; i++)
        {
            partSum += nums[i];
        }
        return partSum;
    },
    (partSum) =>
    {
        Interlocked.Add(ref total, partSum);
    }
);

分区程序将为每个任务创建最佳作业部分,使用线程的服务任务将有更少的时间。如果可以的话,请对此解决方案进行基准测试,并告诉我们它是否能更好地加速。

答案 3 :(得分:1)

每个例子的foreach vs parallel

    for (int i = 0; i < 10; i++)
    {
        int[] array = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29 };
        Stopwatch watch = new Stopwatch();
        watch.Start();
        //Parallel foreach
        Parallel.ForEach(array, line =>
        {
            for (int x = 0; x < 1000000; x++)
            {

            }

        });

        watch.Stop();
        Console.WriteLine("Parallel.ForEach {0}", watch.Elapsed.Milliseconds);
        watch = new Stopwatch();
        //foreach
        watch.Start();
        foreach (int item in array)
        {
            for (int z = 0; z < 10000000; z++)
            {

            }
        }
        watch.Stop();
        Console.WriteLine("ForEach {0}", watch.Elapsed.Milliseconds);

        Console.WriteLine("####");
    }
    Console.ReadKey();

enter image description here

我的CPU

英特尔®酷睿™i7-620M处理器(4M高速缓存,2.66 GHz)