我正在尝试使用蒙特卡罗方法计算Pi中的.NET 4中的新并行度工具。
(实际算法并不那么重要,但为了清楚起见,这里是:
numIterations
个随机点。numIterations
,PI=4 * iterationsInsideCircle / numIterations
。)我有一个方法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
的版本确实有效,但非常非常缓慢,因为前面提到的上下文切换(在前两种方法中没有出现)。
有人能够告诉我为什么会这样吗?
答案 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版本等效,就像您在手动任务版本中所做的那样。