并行框架,避免虚假共享
Posted
技术标签:
【中文标题】并行框架,避免虚假共享【英文标题】:Parallel Framework and avoiding false sharing 【发布时间】:2015-07-01 02:05:59 【问题描述】:最近,我回答了一个关于优化可能的并行化方法以生成任意基数的每个排列的问题。我发布了一个类似于 Parallelized, bad implementation 代码块列表的答案,几乎立即有人指出了这一点:
这几乎可以保证给您虚假共享,并且可能会慢很多倍。 (感谢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
as 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
优化相同。
【问题讨论】:
你最好把这个发到programmers 吗?更好的是让 1 成为高尔夫 challenge @lloydm 在***中有这个问题有什么问题?很高兴这里至少有一些有趣的、具有挑战性的问题,而不仅仅是一百万条错误消息或语法问题 @Prokurors 毫无疑问,它既有趣又具有挑战性。我已经了解了虚假分享。再次阅读有效问题后,我同意它在方框中打勾作为有效问题。 反对者,我该如何改进我的问题? 您的实现也不是 List 的最佳选择。您确切知道列表中需要多少个元素,因此您可以在构造函数中设置容量并防止不必要的分配。 【参考方案1】:首先,我对Parallel.For()
和Parallel.ForEach()
的最初假设是错误的。
糟糕的并行实现很可能有 6 个线程都试图一次写入单个 CouncurrentStack()
。使用线程局部变量的良好实现(下面将详细解释)每个任务只访问一次共享变量,几乎消除了任何争用。
当使用Parallel.For()
和Parallel.ForEach()
时,您不能简单地将for
或foreach
循环替换为它们。这并不是说它不能是盲目的改进,而是如果不检查问题并对其进行检测,使用它们就是将多线程抛出一个问题,因为它可能会使其更快。
**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,它被传递给 body
和 localFinally
lambdas。请注意,我不建议使用并行化实现 1 到 100 的总和,而只是有一个简单的示例来使示例简短。
【讨论】:
以上是关于并行框架,避免虚假共享的主要内容,如果未能解决你的问题,请参考以下文章