我有一个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毫秒降低到5795毫秒。
这怎么可能?
最佳答案
这种情况很难弄清楚。一键是查看内存位置。在没有看到您的代码的情况下,不可能确切地说出问题所在,但是我们可以讨论一些使“多线程不那么好”的事情:
在所有NUMA系统中,当内存与处理器X一起放置并且代码在处理器Y上运行(X和Y不是同一处理器)时,每次访问内存都会降低性能。因此,在正确的NUMA节点上分配内存肯定会有所帮助。 (这可能需要一些特殊的代码,例如设置亲和力掩码以及至少提示您想要Numa感知的OS/Runtime系统分配)。至少,请确保您不要简单地处理“第一个线程,然后再启动更多线程”分配的一个大型数组。
更糟糕的是,内存共享或错误共享-因此,如果两个或多个处理器使用同一条缓存行,您将在这两个处理器之间进行乒乓球比赛,其中每个处理器都将执行“我想要内存”在地址“A”处,获取内存内容,对其进行更新,然后下一个处理器将执行相同的操作。
结果仅在12个线程时变差的事实似乎表明,这与“套接字”有关-您正在共享数据,还是数据位于“错误的节点上”。当线程数为12时,您可能会开始使用第二个套接字(更多),这将使这类问题更加明显。
为了获得最佳性能,您需要在本地节点上分配内存,不共享且不锁定。您的第一组结果看起来也不是“理想的”。我有一些(绝对不共享)代码,可以使处理器数量精确地提高n倍,直到处理器用完为止(不幸的是,我的机器只有4个内核,所以性能不是很好,但是仍然好4倍)而不是1核,并且如果我接触过48或64核计算机,则在计算“怪异数字”时会产生48或64个更好的结果)。
编辑:
“套接字问题”有两件事:
所有这些都相当于在维修汽车上工作,但您没有自己的工具箱,因此,每次需要工具时,都必须问问您旁边的同事 Screwdriver ,15mm Spanner 或您需要的任何工具。然后,当您的工作区域变满时,将工具退还给您。这不是一种非常有效的工作方式……如果您拥有自己的工具,那就更好了(至少最常见的一种-每个月只使用一次的那些特殊 Spanner 中的一个不是什么大问题,但是请确保使用普通的10、12和15毫米 Spanner 和一些 Screwdriver 。)当然,如果有四个机制都共享同一工具箱,情况将更加糟糕。这是在四插槽系统中“在一个节点上分配了所有内存”的情况。
现在想象一下,您有一个“ Spanner 盒”,只有一名机械师可以使用 Spanner 盒,因此,如果您需要12毫米 Spanner ,则必须等待旁边的人完成15毫米 Spanner 的使用。如果您具有“错误的缓存共享”,则会发生这种情况-处理器并没有真正使用相同的值,但是由于在缓存行中存在多个“事物”,因此处理器共享了缓存行( Spanner 框) 。