更便宜的便宜的线程安全计数器?

时间:2015-07-17 17:20:53

标签: c# multithreading cpu-cache interlocked mesi

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

这一直困扰着我,我认为问题在于我在小数据片段上做了大量相对便宜(< 1量子)的任务,这些片段很好地分散并可能很好用缓存局部性,从而以最佳方式运行。基于我对MESI知之甚少,我最好的猜测是LOCK中的x86 Interlocked.Increment前缀将高速缓存行推入独占模式并强制其他内核上的高速缓存未命中并强制每次并行传递时高速缓存重新加载只是为了增加这个计数器。有100ns-ish延迟缓存未命中和我的工作量似乎加起来。 (然后,我可能是错的)

现在,我没有看到解决方法,但也许我错过了一些明显的东西。我甚至考虑使用n个计数器(对应于并行化程度),然后在特定核心上递增每个,但是它似乎不可行(检测我所使用的核心可能会更昂贵,更不用说详细的if / then / else结构和搞乱执行管道)。关于如何打破这头野兽的任何想法? :)

2 个答案:

答案 0 :(得分:2)

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

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

LOCK前缀的作用是限制处理器在等待锁定指令完成执行时可以执行的推测工作量。英特尔64和IA-32架构软件开发人员手册的8.1.2节说:

  

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

在正常情况下,处理器能够在等待解决高速缓存未命中时推测性地执行指令。但是LOCK前缀可以防止这种情况,并且在锁定指令完成执行之前基本上会使管道停止运行。

答案 1 :(得分:2)

来自同一缓存线上的多个核心的操作在硬件中竞争。锁定和常规内存访问都是如此。这是一个真正的问题。添加更多核心时,竞争访问根本不会扩展。缩放通常是非常消极的。

您需要在大多数情况下使用自己的每个核心使用多个缓存行。

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

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

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

您可以使用List<Holder>代替随机索引,并在更多线程加入处理时添加项目。