OpenMP性能

首先,我知道经常会问这种[类型]问题,所以让我先说一下我尽可能多地阅读,但我仍然不知道这笔交易是什么。

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

代码看起来像这样;

 #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  [pid 58066]  ) = -1 EAGAIN (Resource temporarily unavailable) [pid 58065]  ) = -1 EAGAIN (Resource temporarily unavailable) [pid 57684]  ) = 0 [pid 58067]  ) = 0 [pid 58066] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 58067] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 58066]  ) = 0 [pid 57684] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL  [pid 58065]  ) = 0 [pid 58067]  ) = 0 [pid 57684]  ) = -1 EAGAIN (Resource temporarily unavailable) [pid 58066] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL  [pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 58066]  ) = -1 EAGAIN (Resource temporarily unavailable) [pid 57684] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 58065]  ) = 0 [pid 58066] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 57684]  ) = 0 [pid 58067] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL  [pid 58066]  ) = 0 [pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 58067]  ) = -1 EAGAIN (Resource temporarily unavailable) [pid 58066] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL  [pid 57684] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 58065]  ) = 0 [pid 58067] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 58066]  ) = -1 EAGAIN (Resource temporarily unavailable) [pid 57684]  ) = 0 [pid 58067]  ) = 0 [pid 58066] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 58065] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL  [pid 58066]  ) = 0 [pid 58065]  ) = -1 EAGAIN (Resource temporarily unavailable) [pid 58066] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL  [pid 57684] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 58067] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL  [pid 58066]  ) = -1 EAGAIN (Resource temporarily unavailable) [pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1  [pid 57684]  ) = 0 

任何人有什么想法意味着什么? 看起来线程过于频繁地进行上下文切换,或者只是阻塞和解除阻塞? 当我将OMP_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 

我们看到了最终的黯淡回报,尽管这并不奇怪,因为我们在这个测试数据上迭代的几个数据集有<16个成员(因此,我们生成16个线程,比如说for-loop with 7次迭代)。

那么,现在问题仍然存在 – 为什么集群的性能会如此糟糕地降低。 我今晚要尝试不同的四核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,因此绝对有可能实现并行性…

因此,在进行了一些相当广泛的分析之后(感谢这篇关于gprof和gdb时间采样信息的post ),其中包括编写一个大包装函数来生成用于分析的生产级代码,很明显,在绝大多数时候我使用gdb中止了正在运行的代码并运行了backtrace堆栈是在STL 调用中,以某种方式操作向量。

代码将一些向量作为私有变量传递到parallel部分,这似乎工作正常。 但是,在拔出所有向量并用数组(以及其他一些jiggery-pokery来替换它们)之后,我看到了显着的加速。 对于小的人工数据集,速度接近完美(即,当你将一半时间的线程数加倍时),而对于真实数据集,速度提升并不是那么好,但这在上下文中完全有意义代码如何工作。

似乎无论出于何种原因(可能是STL实现中深层的一些静态或全局变量?),当有循环并行移动数十万次迭代时,会出现一些深层锁定,这种情况发生在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结束时实现单个同步事件

  • 删除所有mallocing并从并行区域内freeing – 没有帮助解决这个问题,但确实提供了一个小的通用加速

  • 尝试过各种体系结构和操作系统 – 最终没有真正帮助,但确实表明这是一个Linux与OSX的问题而不是超级计算机与桌面系统的问题

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

  • 构建了一个数据模拟器来复制生产数据负载

  • gprof分析

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

  • 注释掉矢量操作

  • 如果这不起作用, Arne Babenhauserheide的链接看起来可能在OpenMP的内存碎片问题上有一些关键的东西

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

线程使用不同的对象,但这些对象恰好在内存中足够接近它们落在同一个高速缓存行上,并且高速缓存系统将它们视为单个块,它被硬件写锁有效保护,只有一个核可以保存在时间

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

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

特别是例程正在执行大量malloc / free的事实可能导致这种情况。

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

由于线程实际上不进行交互,因此您只需将代码更改为多处理即可。 您最终只会传递消息,并且可以保证线程不需要同步任何内容。

这里的python3.2代码基本上是这样做的(你可能不希望因为性能原因而不能在python中执行它 – 或者将for-loop放入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) 

而已。 将进程数增加到可以放入集群的作业数,并让每个进程只提交和监视作业,以扩展到任意数量的调用。

没有交互和小输入值的巨大function几乎可以用于多处理。 只要你有这个,切换到MPI(几乎无限扩展)并不是太难。

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