【问题标题】:OpenMP performanceOpenMP 性能
【发布时间】:2012-06-11 22:35:14
【问题描述】:

首先,我知道这种 [type of] 问题经常被问到,所以让我先说我已经阅读了尽可能多的内容,但我仍然不知道这是什么问题。

我已经并行化了一个 massive 外部 for 循环。循环迭代的次数变化,通常在 20-150 之间,但循环体做了大量的工作,调用了很多本地密集的线性代数例程(例如,代码是源代码的一部分,而不是外部依赖项) .在循环体中,有 1000 多个对这些例程的调用,但它们完全相互独立,所以我认为这将是并行性的主要候选者。循环代码是C++,但它调用了很多用C编写的子程序。

代码如下所示;

<declare and initialize shared variables here>
#ifdef _OPENMP
#pragma omp parallel for                            \
  private(....)\
  shared(....)              \
  firstprivate(....) schedule(runtime)
#endif
  for(tst = 0; tst < ntest; tst++) {

     // Lots of functionality (science!)
     // Calls to other deep functions which manipulate private variables only
     // Call to function which has 1000 loop iterations doing matrix manipulation
     // With no exaggeration, there are probably millions 
     // of for-loop iterations in this body, in the various functions called. 
     // They also do lots of mallocing and freeing
     // Finally generated some calculated_values

     shared_array1[tst] = calculated_value1;
     shared_array2[tst] = calculated_value2;
     shared_array3[tst] = calculated_value3;

 } // end of parallel and for

// final tidy up

我相信,根本不应该有任何同步-线程访问共享变量的唯一时间是shared_arrays,它们访问这些数组中的唯一点,索引为tst.

问题是,当我增加线程数时(在多核集群上!)我们看到的速度(我们调用此循环 5 次)如下;

              Elapsed time   System time
 Serial:        188.149          1.031
 2 thrds:       148.542          6.788
 4 thrds:       309.586        424.037       # SAY WHAT?
 8 thrds:       230.290        568.166  
16 thrds:       219.133        799.780 

可能值得注意的是系统时间在 2 到 4 个线程之间的巨大跳跃,并且随着我们从 2 到 4 移动,经过的时间翻倍,然后慢慢减少。

我尝试过使用大量OMP_SCHEDULE 参数,但没有运气。这是否与每个线程大量使用 malloc/new 和 free/delete 的事实有关?这一直以 8GB 内存运行 - 但我猜这不是问题。坦率地说,系统时间的大幅增加使得线程看起来可能会阻塞,但我不知道为什么会发生这种情况。

更新 1 我真的认为错误共享会成为问题,所以重新编写了代码,以便循环将它们的计算值存储在线程本地数组中,然后将这些数组复制到最后的共享数组中。遗憾的是这并没有产生任何影响,尽管我自己几乎不相信。

按照@cmeerw 的建议,我运行了 strace -f,在所有初始化之后只有数百万行

[pid 58067] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58066] <... futex resumed> )       = -1 EAGAIN (Resource temporarily unavailable)
[pid 58065] <... futex resumed> )       = -1 EAGAIN (Resource temporarily unavailable)
[pid 57684] <... futex resumed> )       = 0
[pid 58067] <... futex resumed> )       = 0
[pid 58066] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58067] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58066] <... futex resumed> )       = 0
[pid 57684] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid 58065] <... futex resumed> )       = 0
[pid 58067] <... futex resumed> )       = 0
[pid 57684] <... futex resumed> )       = -1 EAGAIN (Resource temporarily unavailable)
[pid 58066] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58066] <... futex resumed> )       = -1 EAGAIN (Resource temporarily unavailable)
[pid 57684] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58065] <... futex resumed> )       = 0
[pid 58066] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 57684] <... futex resumed> )       = 0
[pid 58067] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid 58066] <... futex resumed> )       = 0
[pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58067] <... futex resumed> )       = -1 EAGAIN (Resource temporarily unavailable)
[pid 58066] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid 57684] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58065] <... futex resumed> )       = 0
[pid 58067] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58066] <... futex resumed> )       = -1 EAGAIN (Resource temporarily unavailable)
[pid 57684] <... futex resumed> )       = 0
[pid 58067] <... futex resumed> )       = 0
[pid 58066] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58065] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid 58066] <... futex resumed> )       = 0
[pid 58065] <... futex resumed> )       = -1 EAGAIN (Resource temporarily unavailable)
[pid 58066] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid 57684] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 58067] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid 58066] <... futex resumed> )       = -1 EAGAIN (Resource temporarily unavailable)
[pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
[pid 57684] <... futex resumed> )       = 0

任何人有任何想法是什么意思?看起来线程过于频繁地进行上下文切换,或者只是阻塞和解除阻塞?当我 straceOMP_NUM_THREADS 设置为 0 的相同实现时,我什么都没有。比较一下,使用 1 个线程时生成的日志文件为 486 KB,使用 4 个线程时生成的日志文件为 266 MB。

换句话说,并行版本调用了额外的 4170104 行日志文件...

更新 2

按照 Tom 的建议,我尝试将线程绑定到特定处理器,但无济于事。我们在 OpenMP 3.1 中,所以我使用export OMP_PROC_BIND=true 设置环境变量。相同大小的日志文件和相同的时间范围。

更新 3

情节变厚了。到目前为止只在集群上进行了分析,我通过 Macports 安装了 GNU GCC 4.7 并首次在我的 Macbook 上编译(使用 openMP)(Apple 的 GCC-4.2.1 在启用 OpenMP 时会引发编译器错误,这就是为什么我直到现在还没有在本地编译和并行运行它)。在 Macbook 上,您基本上可以看到您所期望的趋势

                C-code time
 Serial:         ~34 seconds
 2 thrds:        ~21 seconds
 4 thrds:        ~14 seconds
 8 thrds:        ~12 seconds
16 thrds:         ~9 seconds

我们看到接近尾声的回报递减,但这并不奇怪,因为我们在这个测试数据上迭代的几个数据集有

所以,现在问题仍然存在 - 为什么集群的性能会如此严重地下降。今晚我将尝试不同的四核 linuxbox。集群使用 GNU-GCC 4.6.3 编译,但我不敢相信它本身会产生如此大的影响?

集群上既没有安装ltrace 也没有安装GDB(由于各种原因我无法安装它们)。如果我的 linuxbox 提供类似集群的性能,我将在那里运行相应的ltrace 分析。

更新 4

哦,天哪。我决斗将我的 Macbook Pro 引导到 Ubuntu (12.04) 并重新运行代码。这一切都在运行(这有点让人放心),但我看到了我在集群上看到的相同的、奇怪的性能不佳的行为,以及数百万个 futex 调用的相同运行。鉴于我在 Ubuntu 和 OSX 中的本地机器之间的唯一区别是软件(而且我使用相同的编译器和库 - 大概 OSX 和 Ubuntu 没有不同的glibc 实现!)我现在想知道这是否与Linux如何调度/分配线程有关。无论如何,在我的本地机器上让一切变得简单一百万倍,所以我要继续ltrace -f 看看我能找到什么。我为 forks() 关闭一个单独的进程的集群编写了一个解决方法,并在运行时提供了完美的 1/2,因此绝对有可能获得并行性......

【问题讨论】:

  • 您是否在这些函数中生成任何随机数?我有一个类似的问题:glibc 在每次调用 rand 时都会进行同步。 OpenMP 解决方案:stackoverflow.com/questions/8980056/…
  • 你试过在“strace -f”下运行吗?这至少应该告诉你涉及哪些系统调用......
  • 如果您明确设置线程关联掩码,您能看到会发生什么吗?
  • 所以 strace 输出意味着存在严重的锁争用。接下来要做的是查看通过“ltrace -f”运行的输出或使用 gdb 并查看每个线程在随机点的调用堆栈(这应该提供一些线索,说明导致这种过度锁定的原因)。
  • 您的集群硬件配置是什么?它有哪些 CPU - 是多插槽 AMD64 还是 Nehalem 或更新的 Intel 系统?与在具有共享缓存的内核上运行的线程相比,不同 CPU 和/或 NUMA 节点上的线程之间的同步要昂贵得多。您可能还会遇到其他瓶颈,例如内存带宽限制(“...调用大量本地密集的线性代数例程...”提示)

标签: c++ c multithreading openmp


【解决方案1】:

因此,经过一些相当广泛的分析(感谢this great post 提供有关 gprof 和 gdb 时间采样的信息),其中涉及编写一个大型包装函数来生成用于分析的生产级代码,很明显,对于绝大多数当我使用 gdb 中止正在运行的代码并运行 backtrace 时,堆栈处于 STL &lt;vector&gt; 调用中,以某种方式操作向量。

代码将一些向量作为私有变量传递到parallel 部分,这似乎工作正常。然而,在取出所有向量并用数组替换它们(以及其他一些使这项工作正常工作的诡计)之后,我看到了显着的加速。使用小的人工数据集,加速几乎是完美的(即,当你将线程数加倍时,一半的时间),而使用真实数据集,加速并不那么好,但这在上下文中是完全有意义的代码的工作原理。

似乎无论出于何种原因(可能是STL&lt;vector&gt; 实现中的一些静态或全局变量?)当循环通过数十万次并行迭代时,都会出现一些深层锁定,这发生在 Linux 中( Ubuntu 12.01 和 CentOS 6.2),但不在 OSX 中。

我真的很想知道为什么我会看到这种差异。 STL 的实现方式是否有所不同(OSX 版本是在 GNU GCC 4.7 下编译的,Linux 也是如此),或者这与上下文切换有关(如 Arne Babenhauserheide 所建议的)

总的来说,我的调试过程如下;

  • R 内部进行初始分析以识别问题

  • 确保没有static 变量充当共享变量

  • 使用strace -fltrace -f 进行分析,这对于识别锁定是罪魁祸首很有帮助

  • 使用valgrind 分析以查找任何错误

  • 尝试了各种计划类型(自动、引导、静态、动态)和块大小的组合。

  • 尝试将线程绑定到特定处理器

  • 通过为值创建线程本地缓冲区来避免错误共享,然后在for-loop结束时实现单个同步事件

  • 从并行区域中删除了所有 mallocingfreeing - 对问题没有帮助,但确实提供了一个小的总体加速

  • 尝试了各种架构和操作系统 - 最终并没有真​​正的帮助,但确实表明这是 Linux 与 OSX 的问题,而不是超级计算机与桌面的问题

  • 构建一个使用fork() 调用实现并发的版本 - 具有两个进程之间的工作负载。这将 OSX 和 Linux 上的时间减半,这很好

  • 构建数据模拟器以复制生产数据负载

  • gprof 分析

  • gdb 时间采样分析(中止和回溯)

  • 注释掉向量运算

  • 如果这不起作用,Arne Babenhauserheide's link 看起来可能有一些关于 OpenMP 内存碎片问题的关键内容

【讨论】:

  • 现在知道这些向量操作是什么会很有趣。它是否反复尝试调整向量的大小(这当然会由于内存分配而涉及同步)?
  • @cmeerw ^ 是的。我假设考虑到它们都在单独的线程中,这不会是问题,因为内存分配一直在单独的线程中发生,没有任何同步)。
  • @Alex:谢谢你的好回答。现在,请问您所说的“从R [...] 内部进行的初始分析”是什么意思……您是指统计程序吗?如何?!这是什么魔法!? :)
  • 内存分配被阻塞了。想想看,内存是一种共享资源,如果 2 个线程同时分配相同的内存......导致问题的向量(因为它们必须分配以允许调整大小)是可以理解的
  • Linux vs OsX:向量函数调用的 malloc/realloc 例程在操作系统的内核中,因此命中将取决于操作系统对 malloc/realloc 的实现。通过使用预先分配的数组,您可以控制分配的内存和时间,并从内部循环中删除昂贵的内存分配例程。向量并不总是像您发现的那样更好。要获得更多详细信息,我认为您应该深入研究 STL 后见之明是一位出色的老师的来源
【解决方案2】:

如果没有重要的分析,很难确定发生了什么,但性能曲线似乎表明 False Sharing...

线程使用不同的对象,但这些对象碰巧很接近 足够的内存使它们落在同一缓存行上,并且缓存 系统将它们视为一个单一的块,由一个有效的保护 一次只能持有一个内核的硬件写锁

Dobbs 博士关于该主题的精彩文章

http://www.drdobbs.com/go-parallel/article/217500206?pgno=1

尤其是例程执行大量 malloc/free 的事实可能导致这种情况。

一种解决方案是使用基于池的内存分配器而不是默认分配器,以便每个线程倾向于从不同的物理地址范围分配内存。

【讨论】:

  • 但是增加的系统时间宁愿指向锁定争用而不是错误共享(错误共享只会显示增加的用户时间,而不是系统时间)。
  • @cmeerw:你能详细说明原因吗?
  • 非竞争锁定完全在 Linux 的用户空间中完成(因此这些不会显示在系统时间中),另一方面,竞争锁定需要调用内核来暂停/恢复线程(在 Linux 上,这是通过 futex 系统调用完成的)。这就是 strace 输出清楚显示的内容(事实上,由于 futex 系统调用的第一个参数始终相同,它只是一个锁)。另一方面,虚假共享只会减慢代码速度,但不会引入任何额外的系统调用,因此您预计主要会看到用户时间的减慢。
  • 当然,您在系统时间内看到与虚假共享相同的效果的可能性很小 - 但这意味着您实际上会在系统调用中做很多工作(但这不是单线程时序建议)。
【解决方案3】:

由于线程实际上不交互,您可以将代码更改为多处理。你最终只会有消息传递,并且可以保证线程不需要同步任何东西。

这里的 python3.2-code 基本上可以做到这一点(出于性能原因,你可能不想在 python 中这样做 - 或者将 for 循环放入 C 函数并通过 cython 绑定它。你会看到从代码中为什么我无论如何都要在 Python 中显示它):

from concurrent import futures
from my_cython_module import huge_function
parameters = range(ntest)
with futures.ProcessPoolExecutor(4) as e:
    results = e.map(huge_function, parameters)
    shared_array = list(results)

就是这样。将进程数增加到可以放入集群的作业数,并让每个进程只需提交和监视一个作业即可扩展到任意数量的调用。

没有交互的巨大函数和小的输入值几乎需要多处理。一旦你有了它,切换到 MPI(几乎无限扩展)就不会太难了。

从技术方面来看,Linux 中的 AFAIK 上下文切换非常昂贵(具有大量内核空间内存的单片内核),而在 OSX 或 Hurd(Mach 微内核)上要便宜得多。这可能解释了您在 Linux 上看到的大量系统时间,但在 OSX 上却没有。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-03-09
    • 2011-11-07
    • 2017-08-09
    • 1970-01-01
    • 1970-01-01
    • 2021-10-03
    • 2018-09-22
    相关资源
    最近更新 更多