7

我已阅读此主题:C# Thread safe fast(est) counter并在我的并行代码中实现了此功能。据我所见,一切正常,但是它显着增加了处理时间,大约增加了 10%。

这一直困扰着我,我认为问题在于我正在对小数据片段执行大量相对便宜(<1 个量子)的任务,这些小数据片段被很好地分割并且可能充分利用了缓存局部性,从而以最佳方式运行。根据我对 MESI 的了解,我最好的猜测是 x86LOCK前缀Interlocked.Increment将缓存线推入独占模式并强制其他内核上的缓存未命中并强制在每个并行通道上重新加载缓存,只是为了增加这个计数器。由于缓存未命中的 100ns 延迟和我的工作量,它似乎加起来了。(再一次,我可能是错的)

现在,我看不到解决方法,但也许我遗漏了一些明显的东西。我什至在考虑使用 n 个计数器(对应于并行化程度),然后在特定内核上递增每个计数器,但这似乎不可行(检测我在哪个内核上可能会更昂贵,更不用说复杂的 if/then/else结构并弄乱执行管道)。关于如何打破这个野兽的任何想法?:)

4

2 回答 2

2

我想我会澄清一下缓存一致性以及LOCK前缀在英特尔架构中的作用。由于评论太长而且还回答了您提出的一些观点,我认为将其作为答案发布是合适的。

在 MESI 缓存一致性协议中,任何对缓存行的写入都会导致状态变为独占,无论您是否使用LOCK前缀。因此,如果两个处理器都重复访问同一个缓存行,并且至少有一个处理器正在执行写入操作,那么处理器在访问它们共享的行时会遇到缓存行未命中。而如果他们都只从该行读取,那么他们将有缓存行命中,因为他们都可以将行保持在其私有 L1 缓存中处于共享状态。

前缀的LOCK作用是限制处理器在等待锁定指令完成执行时可以做的推测工作量。Intel 64 and IA-32 Architectures Software Developer's Manual 第 8.1.2 节说:

相对于所有其他内存操作和所有外部可见事件,锁定操作是原子操作。只有取指令和页表访问才能传递锁定指令。锁定指令可用于同步一个处理器写入和另一个处理器读取的数据。

在正常情况下,处理器能够在等待缓存未命中解决的同时推测性地执行指令。但是LOCK前缀阻止了这种情况,并且基本上停止了流水线,直到锁定的指令完成执行。

于 2015-07-19T20:22:04.667 回答
2

来自同一高速缓存行上的多个内核的操作在硬件中竞争。这适用于锁定和常规内存访问。这是一个真正的问题。当添加更多内核时,竞争访问根本不会扩展。缩放通常是硬负的。

您需要使用多个缓存线,每个内核大部分时间都使用自己的内核。

你可以使用ThreadLocal<Holder>and class Holder { public int I; }ThreadLocal支持枚举所有已创建的实例,以便对它们进行求和。您还可以使用填充到缓存行大小的结构。这样更安全。

请注意,每个内核使用一个计数器并不重要。每个线程就足够了,因为与增量操作相比,时间量非常长。一些错误的访问不是性能问题。

更快的选择是使用Holder[]. 每个线程绘制一次随机数组索引,然后访问该持有者对象。数组索引比线程本地访问更快。如果您使用的持有者实例的数量(10 倍)比线程数大得多,那么争用就会很小。大多数写入将进入相同的已缓存行。

List<Holder>当更多线程加入处理时,您可以使用 a 并添加项目,而不是随机索引。

于 2015-07-19T21:13:43.760 回答