并行框架并避免错误共享

时间:2015-04-23 05:03:18

标签: c# performance parallel-processing false-sharing

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

  

这几乎可以保证为您提供虚假共享,并且可能会慢很多倍。 (信用到gjvdkamp

他们是对的,这是死亡慢。也就是说,我研究了这个主题,并找到了一些interesting material and suggestions(仅归档的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<Bytes>();
  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<Bytes>();
  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<Bytes>>();
  var limits = new byte[] { 6, 16, 16, 16, 32, 8, 8, 8 };

  Parallel.For (0, limits[0], () => new List<Bytes>(), 
    (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 < limits[7]; h++)
        localList.Add(new Bytes {
          A = (byte)a, B = b, C = c, D = d,
          E = e, F = f, G = g, H = h
        });
      return localList;
  }, x => {
    data.Push(x);
  });

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

对于实际上比单线程版本更快的并行化实现,我的问题是,是否有可以应用于操作的进一步优化?我对与优化相关的想法感到疑惑并行化,而不是用于计算值的算法的改进。具体来说:

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

1 个答案:

答案 0 :(得分:1)

首先,我对Parallel.For()Parallel.ForEach()的初步假设是错误的。

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

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

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

如果您有与Parallel.For()Parallel.ForEach()并行化的操作,那么使用此重载可能是个好主意:

public static ParallelLoopResult For<TLocal>(
    int fromInclusive,
    int toExclusive,
    Func<TLocal> localInit,
    Func<int, ParallelLoopState, TLocal, TLocal> body,
    Action<TLocal> 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的求和,但只是有一个简单的例子来使示例简短。