Parallel.For为这种情况造成巨大的上下文切换开销......为什么?

时间:2011-04-05 13:31:08

标签: c# .net parallel-processing

我正在尝试使用蒙特卡罗方法计算Pi中的.NET 4中的新并行度工具。

(实际算法并不那么重要,但为了清楚起见,这里是:

  1. 在单位广场内挑选numIterations个随机点。
  2. 计算位于由该方块限定的圆内的这些点的数量(即距离方形中心的距离小于0.5的点)
  3. 然后,对于非常大的numIterationsPI=4 * iterationsInsideCircle / numIterations。)
  4. 我有一个方法int ThrowDarts(int numDarts),它在单位正方形内部选择numDarts个随机点(如上所述)并返回单位圆内的点数:

        protected static int ThrowDarts(int iterations)
        {
            int dartsInsideCircle = 0;
            Random random = new Random();
            for (int iteration = 0; iteration < iterations; iteration++)
            {
                double pointX = random.NextDouble() - 0.5;
                double pointY = random.NextDouble() - 0.5;
    
                double distanceFromOrigin = Math.Sqrt(pointX*pointX + pointY*pointY);
                bool pointInsideCircle = distanceFromOrigin <= 0.5;
    
                if (pointInsideCircle)
                {
                    dartsInsideCircle++;
                }
            }
            return dartsInsideCircle;
        }
    

    基本上,在我的每个不同实现中(每个都使用不同的并行机制),我正在编写不同的方法来投掷和计算圆内的飞镖。

    例如,我的单线程实现很简单:

        protected override int CountInterationsInsideCircle()
        {
            return ThrowDarts(_numInterations);
        }
    

    对于我的一个并行算法,我也有这种方法:

        protected override int CountInterationsInsideCircle()
        {
            Task<int>[] tasks = new Task<int>[_numThreads];
    
            for (int i = 0; i < _numThreads; i++)
            {
                tasks[i] = Task.Factory.StartNew(() => ThrowDarts(_numInterations/_numThreads));
            }
    
            int iterationsInsideCircle = 0;
            for (int i = 0; i < _numThreads; i++)
            {
                iterationsInsideCircle += tasks[i].Result;
            }
    
            return iterationsInsideCircle;
        }
    

    希望你能得到这张照片。

    在这里,我遇到了我的难题。我写的Parallel.For版本会导致大量的上下文切换。代码如下:

        protected override int CountInterationsInsideCircle()
        {
            ConcurrentBag<int> results = new ConcurrentBag<int>();
            int result = 0;
    
            Parallel.For(0, _numInterations,
                         // initialise each thread by setting it's hit count to 0
                         () => 0,
                         //in the body, we throw one dart and see whether it hit or not
                         (iteration, state, localState) => localState + ThrowDarts(1),
                         // finally, we sum (in a thread-safe way) all the hit counts of each thread together
                         results.Add);
    
            foreach(var threadresult in results)
            {
                result+=threadresult;
            }
    
            return result;
        }
    

    使用Parallel.For的版本确实有效,但非常非常缓慢,因为前面提到的上下文切换(在前两种方法中没有出现)。

    有人能够告诉我为什么会这样吗?

4 个答案:

答案 0 :(得分:2)

我实际上找到了问题的解决方案。

以前,在我的ThrowDarts方法中,我每次调用都会创建一个新的Random(这是因为Random类不是线程安全的。)

然而,事实证明,这是相对昂贵的。 (至少,只执行一次飞镖投掷时,我们为每次迭代生成一个新的Random。)

因此,我修改了ThrowDarts方法以获取调用者创建的Random,并修改了我的LoopState以包含它自己的Random。

因此,Parallel.For中的每个帖子都包含它自己的Random。我的新实现如下:

    protected override int CountInterationsInsideCircle()
    {
        ConcurrentBag<int> results = new ConcurrentBag<int>();
        Parallel.For(0, _numInterations,
                     // initialise each thread by setting it's hit count to 0
                     () => new LoopThreadState(),
                     // in the body, we throw one dart and see whether it hit or not
                     (iteration, _, localState) =>
                        {
                            localState.Count += ThrowDarts(1, localState.RandomNumberGenerator);
                            return localState;
                        },
                     // finally, we sum (in a thread-safe way) all the hit counts of each thread together
                     result => results.Add(result.Count));

        int finalResult = 0;
        foreach (int threadresult in results)
        {
            finalResult += threadresult;
        }

        return finalResult;
    }

我认为上下文切换指标有点像红色鲱鱼,简单的配置文件就可以完成。漂亮的曲线球,.NET,不错。无论如何,吸取了教训!

谢谢大家, 亚历

答案 1 :(得分:0)

猜测 - 与本地跟踪其结果然后在最后组合它们的其他实现形成对比,并行是使用共享的一组结果,这将为了保持线程而付出更高的代价-safe,更不用说它可能遭受缓存行共享(http://msdn.microsoft.com/en-us/magazine/cc872851.aspx)。

答案 2 :(得分:0)

<强>更新 忍不住在我的家用电脑(linux 32bit,Q9550)上使用单声道2.8.2运行相同的基准测试,只是为了好玩

[mono] /tmp @ dmcs MonteCarlo.cs 
[mono] /tmp @ time mono ./MonteCarlo.exe 
Yo
Approx: 392711899/500000000 => Pi: 3.141695192

real    0m28.109s
user    0m27.966s
sys 0m0.152s
[mono] /tmp @ dmcs MonteCarlo.cs # #define PARALLEL added
[mono] /tmp @ time mono ./MonteCarlo.exe 
Yo
Approx: 392687018/500000000 => Pi: 3.141496144

real    0m8.139s
user    0m31.506s
sys 0m0.064s

所以是的,它似乎按预期扩展。 谢谢你让我真正把它用于单声道'使用'。它在我的'TODO'列表上的时间太长了,它就像一个魅力!

原帖

我只是在双核(E5300)Windows XP上使用单声道2.8.2进行计时

使用并行版本(#define PARALLEL)它以40s运行

使用顺序版本(未定义PARALLEL)大约需要45秒。

所以我没有看到你的测量开销;或者至少我没有看到减速。我也像你一样错过了加速。

在并行运行中,我看到两个CPU都固定为100%,而单线程版本使用约为100%。两个CPU平均50%。

#define PARALLEL
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace test
{
    class MainClass
    {
        const int _numInterations = 50000;
        const int _dartsPerIter = 10000;

        protected static int ThrowDarts (int iterations)
        {
            Random random = new Random ();
            int dartsInsideCircle = 0;
            for (int iteration = 0; iteration < iterations; iteration++) {
                double pointX = random.NextDouble () - 0.5;
                double pointY = random.NextDouble () - 0.5;

                double distanceFromOrigin = Math.Sqrt (pointX * pointX + pointY * pointY);
                bool pointInsideCircle = distanceFromOrigin <= 0.5;

                if (pointInsideCircle) {
                    dartsInsideCircle++;
                }
            }
            return dartsInsideCircle;
        }
        protected int CountInterationsInsideCircle ()
        {
            ConcurrentBag<int> results = new ConcurrentBag<int> ();
            int result = 0;

            // initialise each thread by setting it's hit count to 0
            //in the body, we throw one dart and see whether it hit or not
            // finally, we sum (in a thread-safe way) all the hit counts of each thread together
#if PARALLEL
            Parallel.For (0, _numInterations, () => 0, (iteration, state, localState) => localState + ThrowDarts (_dartsPerIter), results.Add);
#else
            for (var i =0; i<_numInterations; ++i)
                results.Add(ThrowDarts (_dartsPerIter));
#endif

            foreach (var threadresult in results) {
                result += threadresult;
            }

            return result;
        }
        public static void Main (string[] args)
        {
            Console.WriteLine("Yo");
            var inside = new MainClass ().CountInterationsInsideCircle ();
            Console.WriteLine("Approx: {0}/{1} => Pi: {2}",
                               inside, _numInterations * _dartsPerIter,
                               (4.0*inside)/(1.0*_numInterations*_dartsPerIter));
        }
    }
}

答案 3 :(得分:0)

在手册任务案例中_numThreads == _numIterations时会发生什么?第一种方法专门将其拆分为_numThreads,其中Parallel.For版本将始终创建_numIterations任务,每次迭代一次。根据迭代次数的不同,这可能会破坏线程池,并且由于池的争用开销及其相关锁定而无法实现并行性的任何好处。

Parallel.For非常适合每个操作相当昂贵并且可以独立计算。在这种情况下的问题是,为单次迭代运行计算是一种廉价的操作,因此开销开始占据每个任务的时间。您可以使用_numThreads和_numIterations / _numThreads使Parallel.For版本等效,就像您在手动任务版本中所做的那样。