为什么并行化会如此显着地降低性能?

时间:2014-02-12 23:03:18

标签: c++ multithreading performance parallel-processing

我有一个OpenMP程序(数千行,这里不可能重现),其工作方式如下:

它由工作线程和任务队列组成 任务包括卷积;每当工作线程从工作队列中弹出一个任务时,它就会执行所需的卷积,并可选择将更多的卷积推送到队列中。
(没有特定的“主”线程;所有工人都是平等的。)

当我在自己的机器上运行这个程序(4-core HT non-NUMA Core i7)时,我得到的运行时间是:

(#threads: running time)
 1: 5374 ms
 2: 2830 ms
 3: 2147 ms
 4: 1723 ms
 5: 1379 ms
 6: 1281 ms
 7: 1217 ms
 8: 1179 ms

这是有道理的。

然而,当我在NUMA 48核AMD Opteron 6168机器上运行时,我得到了这些运行时间:

 1: 9252 ms
 2: 5101 ms
 3: 3651 ms
 4: 2821 ms
 5: 2364 ms
 6: 2062 ms
 7: 1954 ms
 8: 1725 ms
 9: 1564 ms
10: 1513 ms
11: 1508 ms
12: 1796 ms  <------ why did it get worse?
13: 1718 ms
14: 1765 ms
15: 2799 ms  <------ why did it get *so much* worse?
16: 2189 ms
17: 3661 ms
18: 3967 ms
19: 4415 ms
20: 3089 ms
21: 5102 ms
22: 3761 ms
23: 5795 ms
24: 4202 ms

这些结果非常一致,它不是机器负载的假象 所以我不明白:
在12核之后会导致性能下降的原因是什么?

我会理解,如果性能饱和在某种程度上(我可以将其归咎于有限的内存带宽),但我不明白它如何掉落从1508通过添加更多线程,ms到5795 ms。

这怎么可能?

2 个答案:

答案 0 :(得分:8)

这种情况很难弄明白。一个关键是查看内存位置。如果没有看到你的代码,就不可能完全说出出了什么问题,但我们可以讨论一些让“多线程不太好”的事情:

在所有NUMA系统中,当内存位于处理器X且代码在处理器Y上运行时(其中X&amp; Y不是同一处理器),每次内存访问都会对性能造成不利影响。因此,在正确的NUMA节点上分配内存肯定会有所帮助。 (这可能需要一些特殊的代码,例如设置亲和力掩码,至少暗示您想要Numa感知分配的OS /运行时系统)。至少,确保您不是简单地处理由“第一个线程分配的一个大型数组,然后启动更多线程”。

更糟糕的是共享或错误共享内存 - 所以如果两个或多个处理器使用相同的缓存行,那么在这两个处理器之间将获得乒乓匹配,每个处理器将执行“我希望内存在地址A“,获取内存内容,更新它,然后下一个处理器将执行相同的操作。

结果在12个线程中变坏的事实似乎表明它与“套接字”有关 - 要么是共享数据,要么数据位于“错误的节点上”。在12个线程中,您可能开始使用第二个套接字(更多),这将使这些问题更加明显。

为获得最佳性能,您需要在本地节点上分配内存,不需要共享,也不需要锁定。你的第一组结果看起来也不是“理想的”。我有一些(绝对非共享)代码,它给处理器数量提供了n倍的好处,直到我用完处理器(不幸的是,我的机器只有4个内核,所以它不是很好,但它仍然好4倍超过1核心,如果我得到了48或64核机器,那么在计算“怪异数字”时会产生48或64个更好的结果。

编辑:

“套接字问题”是两件事:

  1. 内存位置:基本上,内存连接到每个套接字,因此如果内存是从属于“上一个”套接字的区域分配的,那么您将获得读取内存的额外延迟。

  2. 缓存/共享:在处理器中,有“快速”链接来共享数据(通常是“底层共享缓存”,例如L3缓存),它允许套接字内的核心共享数据比在不同插槽中的那些更有效。

  3. 所有这些都等于维修汽车,但你没有自己的工具箱,所以每次你需要一个工具时,你都要问你旁边的同事用螺丝刀,15mm扳手,或者其他什么你需要。然后在工作区域满员时将工具返回。这不是一种非常有效的工作方式......如果你拥有自己的工具(至少是最常见的工具 - 你每月只使用一次的特殊扳手之一不是一个大问题,那就更好了,但你常见的10,12和15毫米扳手和一些螺丝刀,肯定)。当然,如果有四种机制,共享相同的工具箱,情况会更糟。这是在四插槽系统中“在一个节点上分配所有内存”的情况。

    现在想象你有一个“扳手盒”,只有一个机械师可以使用扳手盒,所以如果你需要一个12毫米扳手,你必须等待你旁边的人完成使用15毫米扳手。如果您有“虚假缓存共享” - 处理器实际上没有使用相同的值,但由于缓存行中有多个“东西”,处理器正在共享缓存行(缓冲区框),会发生这种情况。

答案 1 :(得分:2)

我有两点建议:

1。)在NUMA系统上,您要确保写入的缓冲区与页面边界对齐,也是页面的倍数。页面通常为4096字节。如果在页面之间拆分缓冲区,则会出现错误共享。

http://dl.acm.org/citation.cfm?id=1295483

  

当共享内存并行系统中的处理器在同一个一致性块(缓存行或页面)中引用不同的数据对象时,会发生错误共享,从而导致“不必要的”一致性操作。

和这个链接 https://parasol.tamu.edu/~rwerger/Courses/689/spring2002/day-3-ParMemAlloc/papers/lee96effective.pdf

  

...当可能具有不同访问模式的几个独立对象被分配给相同的可移动存储器单元时发生的错误共享(在我们的例子中,是一个虚拟内存页面)。

因此,例如,如果一个数组是5000字节,你应该使它成为8192字节(2 * 4096)。然后将其与

之类的东西联系起来
float* array = (float*)_mm_malloc(8192, 4096);  //two pages both aligned to a page

在非NUMA系统上,您不希望多个线程写入同一缓存行(通常为64个字节)。这会导致错误共享。在NUMA系统上,您不希望多个线程写入同一页面(通常为4096字节)。

请参阅此处的一些评论Fill histograms (array reduction) in parallel with OpenMP without using a critical section

2.。)OpenMP可以将线程迁移到不同的核心/处理器,因此您可能希望将线程绑定到某些核心/处理器。您可以使用ICC和GCC执行此操作。对于GCC,我认为您希望执行类似GOMP_CPU_AFFINITY=0 2 4...的操作。请参阅此链接What limits scaling in this simple OpenMP program?