【问题标题】:Why would parallelization decrease performance so dramatically?为什么并行化会如此显着地降低性能?
【发布时间】:2014-03-11 14:19:58
【问题描述】:

我有一个 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。

这怎么可能?

【问题讨论】:

  • 我首先要检查的是任务队列的争用情况。但是很多事情都会弄乱并行程序,你可能需要打开一个并行分析器,比如 Tau 或 Scalasca。
  • @RamanShah:但是,竞争是否会突然/戏剧性地增加时间?
  • Hmya,这就是 NUMA 中的“NU”的意思。一旦您越过了最佳位置,您就可以看到处理器互连的成本。将它们视为通过网络连接的独立机器非常重要。不要让他们解决同样的问题。
  • @HansPassant:我明白了。不幸的是,我看不出如何阻止它们解决相同的问题,因为卷积具有任意的相互依赖性(想想 DAG),因此缓冲区必须共享并可供所有线程使用。我无法在“正确”节点上分配内存,因为一旦我这样做并且不同的节点需要读取数据,它将位于错误的节点上(存在相互依赖的 DAG)。那我可以在这里做什么?
  • 好吧,在这一点上,您正在进入一个必须付费(在代码行数、复杂性和可移植性方面)以扩展性能的世界。它可能涉及诸如主进程之类的东西,该进程分解您的 DAG 依赖项,以将正确的数据以最小的串扰获取到地址空间的正确部分。

标签: c++ multithreading performance parallel-processing


【解决方案1】:

这类情况很难弄清楚。一个关键是查看内存局部性。如果没有看到您的代码,就不可能确切地说出了什么问题,但是我们可以讨论一些“多线程不太好”的问题:

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

另一件更糟糕的事情是共享或错误共享内存 - 因此,如果两个或多个处理器使用相同的高速缓存行,您将在这两个处理器之间获得乒乓匹配,每个处理器都会这样做“我想要地址 A" 的内存,获取内存内容,更新它,然后下一个处理器会做同样的事情。

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

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

编辑:

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

  1. 内存局部性:基本上,内存连接到每个套接字,因此如果内存是从属于“前一个”套接字的区域分配的,那么读取内存时会出现额外的延迟。

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

所有这些都相当于维修汽车,但您没有自己的工具箱,因此每次需要工具时,都必须向旁边的同事要螺丝刀、15 毫米扳手或其他任何东西你需要。然后在您的工作区域有点满时归还工具。这不是一种非常有效的工作方式......如果你有自己的工具会更好(至少是最常见的工具 - 你每月只使用一次的那些特殊扳手之一不是大问题,但是你常见的 10、12 和 15 毫米扳手和几把螺丝刀,当然)。当然,如果有四个机制共享同一个工具箱,情况会更糟。这就是在四插槽系统中“在一个节点上分配所有内存”的情况。

现在假设你有一个“扳手盒”,只有一个机械师可以使用扳手盒,所以如果你需要一个 12 毫米扳手,你必须等待旁边的人用完15 毫米扳手。如果您有“错误的缓存共享”,就会发生这种情况 - 处理器并没有真正使用相同的值,但是因为缓存线中有不止一个“东西”,处理器正在共享缓存线(扳手盒) .

【讨论】:

  • +1 谢谢。不幸的是,队列背后的全部原因是卷积依赖于先前的卷积,所以我不能将问题分成单独的部分。有一个依赖关系的 DAG,执行每个卷积都会释放对其父节点的约束,一旦释放了卷积的最后一个约束,父节点就会被放置到工作队列中。每个父级都依赖于其子级的缓冲区,因此我无法在“正确的”NUMA 节点上分配缓冲区,因为问题无法拆分为不相交的部分(它们都是相互依赖的)。
  • 我认为你的插座正中了钉子。我没有意识到这一点,但根据/proc/cpuinfo,12 正是具有相同physical id 的 CPU 数量,所以看起来我们确实开始使用第二个套接字。如果您不介意更深入地讨论套接字问题,我将不胜感激。这与数据在错误的 NUMA 节点上的问题相同,还是不同?
  • 您提到的虚假共享也应该提到页面,而不仅仅是缓存行。这是另一种仅在 NUMA 系统上发生的错误共享。此外,您可以在一个处理器上分配内存并在另一个处理器上使用它,但它应该与页面 (4096) 对齐并且是页面 (4096) 的倍数。就像在非 NUMA 系统上一样。如果您自己在并行块之外分配内存,则应将其对齐为 64 字节并使其成为 64 字节的倍数 stackoverflow.com/questions/21445901/…
  • 读到这里的人也应该take a look here,图值一千字。
【解决方案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 字节)。

在这里查看一些 cmets 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?

【讨论】:

  • 感谢您的回答,但我已经在 cmets 中针对您的建议解决了这两个问题。你能解释一下为什么尽管我在上面解释过,但你认为它们会有用吗?谢谢!
  • 我的意思是与页面边界对齐。当然,它们可能大于 4096 字节,因此如果需要,您可以通过分配额外空间来使数组成为 4096 字节的倍数。例如。如果数组是 5000 字节,则将其设为 8192 字节。在第一个链接中查看 Hristo Iliev 的 cmets。
  • 我还是不明白这跟虚假分享有什么关系。错误共享是指您从同一 缓存行 获取两个变量。一个高速缓存行是 16 个字节。 16 个字节只有 2 个doubles,而我的缓冲区更像是 200 或 2000 个doubles……错误共享的可能性微乎其微。
  • @Mehrdad,我没有说这是虚假分享。我说这就像虚假分享。如果一个处理器必须写入在另一个处理器上共享的页面,那么它就有处理器互连的问题。在非 NUMA 系统上,您不会有多个线程写入同一缓存行。在 NUMA 系统上,您不希望多个线程写入同一页面。
  • @MatsPetersson,您需要知道的一切都可以在这里找到stackoverflow.com/questions/10850155/openmp-for-schedule,而且它只有一岁。
猜你喜欢
  • 1970-01-01
  • 2011-12-15
  • 1970-01-01
  • 2015-09-20
  • 2017-07-07
  • 2020-01-31
  • 1970-01-01
  • 2012-10-02
  • 1970-01-01
相关资源
最近更新 更多