【问题标题】:Even faster inexpensive thread-safe counter?更快的廉价线程安全计数器?
【发布时间】:2015-10-07 12:01:53
【问题描述】:

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

这一直困扰着我,我认为问题在于我正在对小数据片段执行大量相对便宜(Interlocked.Increment 中的 x86 LOCK 前缀将缓存线推入独占模式并强制其他内核上的缓存未命中并强制在每个并行通道上重新加载缓存只是为了增加这个计数器。由于缓存未命中的 100ns 延迟和我的工作量,它似乎加起来了。 (话说回来,我可能错了)

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

【问题讨论】:

  • map-reduce 通常比基于互锁/缓存的方法更容易推理...如果您可以对数据进行分区而不是单独计算每个分区而不是组合结果...
  • 您的意思是将批次拆分为 n 个流并在特定核心上以多个增量处理每个流?但是我没有强制亲和力的方法,不是吗?这意味着我的流现在将超过 1 个量子并且将遍布内核,再次迫使缓存未命中在它们自己的计数器上,不是吗?即使它没有互锁,它也会变脏并强制写入读取。每个计数器的未命中次数会减少 n 倍,但计数器会增加 n 倍。
  • Java 的Striped64 使用分区模型。快速路径(无竞争)很便宜,当检测到竞争时动态增长计数器很复杂。我对它进行了调整,以增加一组环形缓冲区,并且在实践中效果很好。
  • 你能“批量更新”计数器,即处理几个项目并使用Interlocked.Add而不是Interlocked.Increment吗?
  • @mmix 只要您保证批次由一次只由一个线程处理,您不需要围绕批次特定的计数器进行任何锁定。请注意,要求是“一次只有一个”,甚至不是“一个且唯一”或“一个特定的核心”。事实上,你有更多的计数器,但如果你的批次足够大,那应该没问题。

标签: c# multithreading cpu-cache interlocked mesi


【解决方案1】:

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

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

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

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

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

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

    【讨论】:

    • 嗯,索引数组的好主意,我什至有一个值,我可以二进制 AND 以获得跨任务的 2^x 宽的均匀分布。我将用 x 进行试验,看看哪一个能提供更好的结果。我对你的答案投了赞成票,如果这能解决问题,我会接受。
    • 好东西,我避免了随机并使用唯一的任务 id AND 0x1F 来派生要在 32 宽数组中使用的索引,并且性能回升,与没有计数器时看到的速度相当。接受。
    猜你喜欢
    • 2021-06-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多