并行框架并避免错误共享

最近,我回答了一个关于优化可能的可并行化方法来生成任意基数的每个排列的问题。 我发布了类似于Parallelized,糟糕的实现代码块列表的答案,有人几乎立即指出了这一点:

这几乎可以保证为您提供错误的共享,并且可能会慢很多倍。 (信用gjvdkamp )

他们是对的, 死亡很慢。 也就是说,我研究了这个主题,并找到了一些有趣的材料和建议 (仅存档的MSDN杂志, .NET Matters:False Sharing )来对抗它。 如果我理解正确,当线程访问连续的内存(例如,可能支持ConcurrentStack的数组)时,可能会发生错误共享。


对于横向规则下面的代码, Bytes为:

 struct Bytes { public byte A; public byte B; public byte C; public byte D; public byte E; public byte F; public byte G; public byte H; } 

对于我自己的测试,我想获得这个运行的并行版本并且真正更快,所以我创建了一个基于原始代码的简单示例。 6作为limits[0]对我来说是一个懒惰的选择 – 我的计算机有6个核心。

单线程块 平均运行时间:10s0059ms

  var data = new List(); var limits = new byte[] { 6, 16, 16, 16, 32, 8, 8, 8 }; for (byte a = 0; a < limits[0]; a++) for (byte b = 0; b < limits[1]; b++) for (byte c = 0; c < limits[2]; c++) for (byte d = 0; d < limits[3]; d++) for (byte e = 0; e < limits[4]; e++) for (byte f = 0; f < limits[5]; f++) for (byte g = 0; g < limits[6]; g++) for (byte h = 0; h < limits[7]; h++) data.Add(new Bytes { A = a, B = b, C = c, D = d, E = e, F = f, G = g, H = h }); 

并行化,执行不佳 运行时平均值:81s729ms,~8700个争用

  var data = new ConcurrentStack(); var limits = new byte[] { 6, 16, 16, 16, 32, 8, 8, 8 }; Parallel.For(0, limits[0], (a) => { for (byte b = 0; b < limits[1]; b++) for (byte c = 0; c < limits[2]; c++) for (byte d = 0; d < limits[3]; d++) for (byte e = 0; e < limits[4]; e++) for (byte f = 0; f < limits[5]; f++) for (byte g = 0; g < limits[6]; g++) for (byte h = 0; h < limits[7]; h++) data.Push(new Bytes { A = (byte)a,B = b,C = c,D = d, E = e,F = f,G = g,H = h }); }); 

并行化,?? 实现 运行时平均值:5s833ms,92个争用

  var data = new ConcurrentStack<List>(); var limits = new byte[] { 6, 16, 16, 16, 32, 8, 8, 8 }; Parallel.For (0, limits[0], () => new List(), (a, loop, localList) => { for (byte b = 0; b < limits[1]; b++) for (byte c = 0; c < limits[2]; c++) for (byte d = 0; d < limits[3]; d++) for (byte e = 0; e < limits[4]; e++) for (byte f = 0; f < limits[5]; f++) for (byte g = 0; g < limits[6]; g++) for (byte h = 0; h  { data.Push(x); }); 

我很高兴我有一个比单线程版本更快的实现。 我预计结果会接近10s / 6左右,或大约1.6秒,但这可能是一种天真的期望。

我的问题是并行实现实际上比单线程版本更快,是否有可以应用于操作的进一步优化? 我想知道与并行化相关的优化,而不是用于计算值的算法的改进。 特别:

  • 我知道存储和填充struct而不是byte[] ,但它与并行化无关(或者是它?)
  • 我知道使用纹波进位加法器可以延迟评估所需的值,但与struct优化相同。

首先,关于Parallel.For()Parallel.ForEach()初始假设是错误的。

糟糕的并行实现很可能有6个线程都试图一次写入一个CouncurrentStack() 。 使用线程本地的好实现(下面将详细解释)仅在每个任务中访问共享变量一次,几乎消除了任何争用。

使用Parallel.For()Parallel.ForEach()不能简单地用它们替换forforeach循环。 这并不是说它不能盲目改进,但是如果没有检查问题并对其进行检测,使用它们会导致multithreading处理问题,因为它可能会使问题变得更快。

** Parallel.For()Parallel.ForEach()具有重载,允许您为它们最终创建的Task创建本地状态,并在每次迭代执行之前和之后运行表达式。

如果你有一个与Parallel.For()Parallel.ForEach() Parallel.For()的操作,那么使用这个重载可能是个好主意:

 public static ParallelLoopResult For( int fromInclusive, int toExclusive, Func localInit, Func body, Action localFinally ) 

例如,调用For()将所有整数从1加到100,

 var total = 0; Parallel.For(0, 101, () => 0, // <-- localInit (i, state, localTotal) => { // <-- body localTotal += i; return localTotal; }, localTotal => { <-- localFinally Interlocked.Add(ref total, localTotal); }); Console.WriteLine(total); 

localInit应该是一个初始化本地状态类型的lambda,它传递给bodylocalFinally lambdas。 请注意我不建议使用并行化实现1到100的求和,但只是有一个简单的例子来简化示例。