缓存友好的方式从多个线程收集结果

时间:2017-09-12 10:25:04

标签: c++ multithreading optimization x86 cpu-cache

考虑N个线程执行一些具有较小结果值的异步任务,例如doubleint64_t。因此,8个结果值可以适合单个CPU缓存行。 N等于CPU核心数。

一方面,如果我只是分配一组N个项目,每个项目都是doubleint64_t,那么8 threads will share a CPU cache line, which seems inefficient

另一方面,如果我为每个double / int64_t分配一个完整的缓存行,则接收者线程必须获取N缓存行,每个缓存行由不同的CPU写入核心(1除外)。

这种情况是否有效的解决方案? CPU是x86-64。用C ++解决方案是首选。

澄清1 :线程启动/退出开销不大,因为使用了线程池。因此,它主要是在关键部分同步。

澄清2 :并行批次带有依赖关系。主线程只能在收集并处理上一批次的结果后才能启动下一批并行计算。因为前一批次的结果用作下一批次的一些参数。

2 个答案:

答案 0 :(得分:3)

更新:我可能误会了。您是否正在寻找许多小批量工作的快速周转?在这种情况下,您可能最好将每个线程写入其自己的缓存行,或者可以将它们成对分组。如果每个工作线程必须获得独占访问权限(MESI / MESIF / MOESI)以写入同一缓存行,那么将所有内核序列化为某种顺序。

让读者线程从N个线程读取结果可以让所有这些缓存未命中并行发生。

From your comment

  

我想分散并每秒收集数百万次这样的并行计算。换句话说,头部线程分配工作,启动工作线程,然后收集结果,对其执行某些操作,然后再次启动并行计算。

因此,您需要收集数百万个结果,但每个核心只有一个工作线程。因此每个工作线程必须产生~100k的结果。

为每个工作人员提供一个输出数组,它会存储已完成的不同任务的连续结果。实际数组可能只有4k条目长或某些东西,有些一旦读者在该线程的缓冲区的后半部分启动,同步就让编写器回绕并重用上半部分。

当收集器线程从其中一个数组中读取结果时,它会将该缓存行带入其自己的L2 / L1D缓存中,并在同一缓存行中带来7个其他结果(假设通常情况下工作者线程已经填满了所有8个int64_t个插槽,并且不再为这组小任务写入该缓存行。)

或者更好的是,将它们分批收集到与缓存行对齐的批次中,因此冲突未命中在收回它之前不会从收集器的L1D中逐出缓存行。 (通过为每个线程使用不同的偏移量来偏移结果数组,从而减少发生这种情况的可能性,因此收集器线程不会读取N个缓存行,这些缓存行全部相互偏移4kiB或其他的倍数。)< / p>

如果你可以在输出数组中使用sentinel值,那可能是理想的。如果收集器看到它,它知道它超前于工作者并且应该检查其他线程。 (或者,如果它通过所有输出数组而没有找到新结果,则会睡眠)。

否则,需要当前输出位置共享变量,工作人员在写入输出数组后更新(使用发布 - 存储)。 (也许批处理这些位置计数器更新为每8个数组一个结果。但请确保你使用纯原子存储,而不是+= 8。由于生产者线程是唯一写入该变量的人,它会愚蠢地拥有lock add的开销。)

如果打包到一个数组中,这很容易导致工作线程之间的错误共享,并且肯定需要缓存(不在UC或WC内存中,因此工作线程可以有效地重写它)。所以你肯定希望每个线程都有自己的缓存行。收集器将不得不承受读取N个不同缓存行的惩罚(并且可能遭受内存错误推测机器清除:What are the latency and throughput costs of producer-consumer sharing of a memory location between hyper-siblings versus non-hyper siblings?

实际上,在这种情况下,最好的选择可能是在输出数组的每个缓存行中使用8个qwords中的一个作为&#34;完成&#34;标志或位图,因此收集器线程可以检查以查看缓存行中的7结果是否准备就绪。

如果只是在工作线程和收集器线程之间获得结果是你的主要瓶颈,那么你的线程可能太精细了。你应该更粗略地破坏你的任务,或让你的工作线程做它产生的多种结果的一些结合,而它们的L1D仍然很热。这比通过L3或DRAM将其带到另一个核心所带来的更多更好的带宽。

答案 1 :(得分:1)

如果工作线程的访问/写入次数远远超过从头/主线程获得/读取的结果,那么

  • 您必须避免工作人员之间的虚假共享(使用公共缓存行)。这应该通过使用内部工作的自动变量(实际上可以实现为仅寄存器)来完成。
  • 将结果传回主线程(或从主线程输入)的效率较低,并且可能使用阵列(即公共缓存线)。在这里,您可以简单地尝试最有效的方法。