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

C++ 为什么并行化会如此显著地降低性能?,c++,multithreading,performance,parallel-processing,C++,Multithreading,Performance,Parallel Processing,我有一个OpenMP程序(数千行,此处无法复制),其工作原理如下: 它由工作线程和任务队列组成。 任务由一个卷积组成;每当工作线程从工作队列中弹出一个任务时,它都会执行所需的卷积,并可以选择将更多的卷积推到队列上。 (没有特定的“主”螺纹;所有工人都是平等的。) 当我在自己的机器()上运行此程序时,得到的运行时间为: (#threads: running time) 1: 5374 ms 2: 2830 ms 3: 2147 ms 4: 1723 ms 5: 1379 ms 6: 1

我有一个OpenMP程序(数千行,此处无法复制),其工作原理如下:

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

当我在自己的机器()上运行此程序时,得到的运行时间为:

(#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
1:9252毫秒
2:5101毫秒
3:3651毫秒
4:2821毫秒
5:2364毫秒
6:2062毫秒
7:1954毫秒
8:1725毫秒
9:1564毫秒
10:1513毫秒
11:1508毫秒

12:1796ms这种情况很难理解。一个关键是查看内存位置。如果看不到您的代码,就不可能准确地说出哪里出了问题,但我们可以讨论一些amke“多线程不太好”的事情:

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

另一件更糟糕的事情是共享或错误共享内存-因此,如果两个或多个处理器使用相同的缓存线,您将在这两个处理器之间获得乒乓匹配,其中每个处理器将执行“我希望内存位于地址a”,获取内存内容,更新它,然后下一个处理器会做同样的事情

12个线程的结果变差这一事实似乎表明这与“套接字”有关——要么共享数据,要么数据位于“错误的节点”。在12个线程时,很可能开始使用第二个套接字(更多),这将使此类问题更加明显

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

编辑:

“插座问题”有两个方面:

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

  • 缓存/共享:在处理器内,存在共享数据的“快速”链接(通常是“底层共享缓存”,例如L3缓存),这使得套接字中的内核比其他套接字中的内核更高效地共享数据

  • 所有这些都相当于维修汽车,但你没有自己的工具箱,所以每次你需要工具时,你都必须向你旁边的同事要一把螺丝刀、15毫米扳手或任何你需要的东西。然后在你的工作区域有点满的时候把工具还给他。这不是一种非常有效的工作方式……它会如果你有自己的工具就更好了(至少是最常用的一种——一个月只使用一次的专用扳手不是什么大问题,但你常用的10、12和15mm扳手和一些螺丝刀是肯定的)。当然,如果有四个机制共享同一个工具箱,情况会变得更糟。在一个四插槽系统中,“所有内存都分配在一个节点上”


    现在假设你有一个“扳手盒”,只有一个技工可以使用扳手盒,因此如果你需要一个12毫米的扳手,你必须等待你旁边的人使用完15毫米的扳手。如果你有“错误的缓存共享”,就会发生这种情况-处理器实际上没有使用相同的值,但由于缓存线中有多个“东西”,因此处理器共享缓存线(扳手盒)。

    我有两个建议:

    1.)在NUMA系统上,您希望确保写入的缓冲区与页面边界对齐,并且是页面的倍数。页面通常为4096字节。如果在页面之间分割缓冲区,则会得到错误的共享

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

    这个链接呢

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

    因此,例如,如果一个数组为5000字节,则应将其设为8192字节(2*4096)。然后用类似于

    float* array = (float*)_mm_malloc(8192, 4096);  //two pages both aligned to a page
    
    在非NUMA系统上,您不希望多个线程写入同一缓存线(通常为64字节)。这会导致错误共享。在NUMA系统上,您不希望多个线程写入同一页(通常为4096字节)

    请参见此处的一些评论

    2.)OpenMP可以将线程迁移到不同的内核/处理器,因此您可能希望将线程绑定到某些内核/处理器。您可以使用ICC和GCC实现这一点。有了GCC,我想您需要做一些类似于
    GOMP\u CPU\u AFFINITY=02的事情