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

11 浏览
0 Comments

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

我阅读了这个主题:C#线程安全最快的计数器,并在我的并行代码中实现了这个功能。就我所见,一切都正常工作,但它显著增加了处理时间,大约增加了10%左右。

这一点一直困扰着我,我认为问题在于我正在执行大量相对廉价(<1个时钟周期)的任务,这些任务是在小数据片段上进行的,这些数据片段被很好地分区,并且可能很好地利用了缓存局部性,从而实现了最佳运行效果。根据我对MESI的了解,我最好的猜测是,x86中Interlocked.Increment中的LOCK前缀将缓存行推入独占模式,并强制其他核心发生缓存失效并在每个并行传递中强制重新加载缓存,仅仅为了递增这个计数器。根据我的工作负载和大约100纳秒的缓存失效延迟,似乎会累积起来。(然而,我可能是错误的)

现在,我看不到解决办法,但也许我忽略了一些明显的东西。我甚至考虑过使用n个计数器(对应于并行度)然后在特定核心上递增每个计数器,但这似乎不可行(检测我所在的核心可能更昂贵,更不用说复杂的if/then/else结构和对执行流水线的干扰了)。有关如何解决这个问题的任何想法吗? 🙂

0
0 Comments

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

缓存一致性和在Intel架构中的LOCK前缀的作用有些澄清,我想对此进行一些解释。由于这个回答太长了,而且还回答了你提出的一些问题,所以我认为将其作为一个回答发表是合适的。

在MESI缓存一致性协议中,对缓存行的任何写操作都会导致状态变为独占状态,无论是否使用了LOCK前缀。因此,如果两个处理器都重复访问同一缓存行,并且至少有1个处理器进行写操作,那么当访问它们共享的缓存行时,处理器将经历缓存行失效。而如果它们只是从该行中读取数据,那么它们将具有缓存行命中,因为它们可以将该行保留在私有L1缓存中的共享状态。

LOCK前缀的作用是限制处理器在等待锁定指令执行完毕时可以进行的推测性工作的数量。《Intel 64和IA-32体系结构软件开发人员手册》的8.1.2节中写道:

“锁定操作在所有其他内存操作和所有外部可见事件方面都是原子的。只有指令获取和页表访问可以通过锁定指令。锁定指令可用于同步由一个处理器写入的数据和由另一个处理器读取的数据。”

在正常情况下,处理器可以在等待解决缓存失效时进行推测性执行。但是,LOCK前缀会阻止这种情况的发生,并且基本上会使流水线停顿,直到锁定指令执行完毕。

0
0 Comments

在多个核心上对同一缓存行进行操作时,会在硬件上产生争用。这对于锁定和常规内存访问都是真实的问题。当增加更多核心时,争用访问根本无法扩展。通常情况下,扩展性是非常困难的。

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

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

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

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

您还可以使用List<Holder>,并在更多线程加入处理时添加项目。

哦,使用索引数组的好主意,我实际上甚至有一个值,我可以使用它进行二进制AND运算,以在任务之间获得2^x宽的均匀分布。我将尝试不同的x值,看看哪个效果更好。我为您的答案投了赞成票,如果这个方法能解决问题,我会接受它。

很好,我避免了随机选择,并使用唯一的任务ID AND 0x1F来获取在32宽的数组中使用的索引,并且性能回到了之前计数器没有的速度。接受。

0