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

Even faster inexpensive thread-safe counter?

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

这一直困扰着我,我认为问题在于我正在对小数据片段执行大量相对便宜(<1 量子)的任务,这些小数据片段被很好地划分并且可能很好使用缓存局部性,因此 运行 最佳。根据我对 MESI 的了解,我最好的猜测是 Interlocked.Increment 中的 x86 LOCK 前缀将缓存行推入独占模式并强制其他内核上的缓存未命中并强制在每次并行传递时重新加载缓存只是为了增加这个计数器。由于缓存未命中的 100 纳秒延迟和我的工作负载,它似乎加起来了。 (话又说回来,我可能是错的)

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

我想我会提供一些关于高速缓存一致性以及 LOCK 前缀在 Intel 体系结构中的作用的说明。由于评论太长,并且还回答了您提出的一些观点,我认为 post 作为答案是合适的。

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

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

Locked operations are atomic with respect to all other memory operations and all externally visible events. Only instruction fetch and page table accesses can pass locked instructions. Locked instructions can be used to synchronize data written by one processor and read by another processor.

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

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

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

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

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

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

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