multithreading可以抑制编译器优化吗?

我偶然发现有几次将部分程序与OpenMP并行化只是为了注意到最后,尽管具有良好的可扩展性,但由于单线程情况的性能不佳,大多数预见的加速都会丢失(如果与串行版)。

网络上出现的这种行为的常见解释是编译器生成的代码在multithreading情况下可能更糟 。 无论如何,我无法在任何地方找到解释为什么assembly可能更糟的参考。

那么,我想问一下编译器的人是:

multithreading可以抑制编译器优化吗? 万一,性能怎么会受到影响?

如果它可以帮助缩小问题,我主要对高性能计算感兴趣。

免责声明 :正如评论中所述,以下部分答案可能在将来过时,因为他们简要讨论了在提出问题时编译器处理优化的方式。

我认为这个答案充分描述了这个原因,但我会在这里稍微扩展一下。

不过,在此之前,这是关于-fopenmp的gcc 4.8的文档 :

-fopenmp
允许在C / C ++中处理OpenMP指令#pragma omp,在Fortran中处理!$ omp。 指定-fopenmp时,编译器根据OpenMP应用程序接口v3.0 http://www.openmp.org/生成并行代码。 此选项意味着-pthread,因此仅在支持-pthread的目标上受支持。

请注意,它未指定禁用任何function。 实际上,gcc没有理由禁用任何优化。

然而,为什么openmp与1个线程相比没有openmp的原因是编译器需要转换代码,添加函数以便为具有n> 1个线程的openmp的情况做好准备。 让我们想一个简单的例子:

 int *b = ... int *c = ... int a = 0; #omp parallel for reduction(+:a) for (i = 0; i < 100; ++i) a += b[i] + c[i]; 

此代码应转换为以下内容:

 struct __omp_func1_data { int start; int end; int *b; int *c; int a; }; void *__omp_func1(void *data) { struct __omp_func1_data *d = data; int i; d->a = 0; for (i = d->start; i < d->end; ++i) d->a += d->b[i] + d->c[i]; return NULL; } ... for (t = 1; t < nthreads; ++t) /* create_thread with __omp_func1 function */ /* for master thread, don't create a thread */ struct master_data md = { .start = /*...*/, .end = /*...*/ .b = b, .c = c }; __omp_func1(&md); a += md.a; for (t = 1; t < nthreads; ++t) { /* join with thread */ /* add thread_data->a to a */ } 

现在,如果我们使用nthreads==1运行它,代码将有效地减少到:

 struct __omp_func1_data { int start; int end; int *b; int *c; int a; }; void *__omp_func1(void *data) { struct __omp_func1_data *d = data; int i; d->a = 0; for (i = d->start; i < d->end; ++i) d->a += d->b[i] + d->c[i]; return NULL; } ... struct master_data md = { .start = 0, .end = 100 .b = b, .c = c }; __omp_func1(&md); a += md.a; 

那么no openmp版本和单线程openmp版本之间有什么区别?

一个区别是有额外的胶水代码。 需要传递给openmp创建的函数的变量需要放在一起形成一个参数。 因此有一些开销准备函数调用(以及稍后检索数据)

然而更重要的是,现在代码不再是一个整体。 跨function优化还没有那么先进,大多数优化都是在每个function中完成的。 较小的function意味着优化的可能性较小。


为了完成这个答案,我想向您展示-fopenmp如何影响gcc的选项。 (注意:我现在在旧电脑上,所以我有gcc 4.4.3)

运行gcc -Q -v some_file.c给出了这个(相关的)输出:

 GGC heuristics: --param ggc-min-expand=98 --param ggc-min-heapsize=128106 options passed: -v ac -D_FORTIFY_SOURCE=2 -mtune=generic -march=i486 -fstack-protector options enabled: -falign-loops -fargument-alias -fauto-inc-dec -fbranch-count-reg -fcommon -fdwarf2-cfi-asm -fearly-inlining -feliminate-unused-debug-types -ffunction-cse -fgcse-lm -fident -finline-functions-called-once -fira-share-save-slots -fira-share-spill-slots -fivopts -fkeep-static-consts -fleading-underscore -fmath-errno -fmerge-debug-strings -fmove-loop-invariants -fpcc-struct-return -fpeephole -fsched-interblock -fsched-spec -fsched-stalled-insns-dep -fsigned-zeros -fsplit-ivs-in-unroller -fstack-protector -ftrapping-math -ftree-cselim -ftree-loop-im -ftree-loop-ivcanon -ftree-loop-optimize -ftree-parallelize-loops= -ftree-reassoc -ftree-scev-cprop -ftree-switch-conversion -ftree-vect-loop-version -funit-at-a-time -fvar-tracking -fvect-cost-model -fzero-initialized-in-bss -m32 -m80387 -m96bit-long-double -maccumulate-outgoing-args -malign-stringops -mfancy-math-387 -mfp-ret-in-387 -mfused-madd -mglibc -mieee-fp -mno-red-zone -mno-sse4 -mpush-args -msahf -mtls-direct-seg-refs 

并运行gcc -Q -v -fopenmp some_file.c给出这个(相关的)输出:

 GGC heuristics: --param ggc-min-expand=98 --param ggc-min-heapsize=128106 options passed: -v -D_REENTRANT ac -D_FORTIFY_SOURCE=2 -mtune=generic -march=i486 -fopenmp -fstack-protector options enabled: -falign-loops -fargument-alias -fauto-inc-dec -fbranch-count-reg -fcommon -fdwarf2-cfi-asm -fearly-inlining -feliminate-unused-debug-types -ffunction-cse -fgcse-lm -fident -finline-functions-called-once -fira-share-save-slots -fira-share-spill-slots -fivopts -fkeep-static-consts -fleading-underscore -fmath-errno -fmerge-debug-strings -fmove-loop-invariants -fpcc-struct-return -fpeephole -fsched-interblock -fsched-spec -fsched-stalled-insns-dep -fsigned-zeros -fsplit-ivs-in-unroller -fstack-protector -ftrapping-math -ftree-cselim -ftree-loop-im -ftree-loop-ivcanon -ftree-loop-optimize -ftree-parallelize-loops= -ftree-reassoc -ftree-scev-cprop -ftree-switch-conversion -ftree-vect-loop-version -funit-at-a-time -fvar-tracking -fvect-cost-model -fzero-initialized-in-bss -m32 -m80387 -m96bit-long-double -maccumulate-outgoing-args -malign-stringops -mfancy-math-387 -mfp-ret-in-387 -mfused-madd -mglibc -mieee-fp -mno-red-zone -mno-sse4 -mpush-args -msahf -mtls-direct-seg-refs 

采取差异,我们可以看到唯一的区别是使用-fopenmp ,我们定义了-D_REENTRANT (当然-fopenmp启用了-fopenmp )。 所以,请放心,gcc不会产生更糟糕的代码。 只是当需要为线程数大于1并且有一些开销时添加准备代码。


更新:我真的应该在启用优化的情况下对此进行测试。 无论如何,使用gcc 4.7.3,输出相同的命令,添加-O3会给出相同的区别。 因此,即使使用-O3 ,也没有禁用优化。

与OMP的显式编译指示相比,编译器只是不知道代码可以由多个线程执行。 因此,他们既不能使代码更高效,也不能降低效率。

这在C ++中有严重的后果。 这对图书馆作者来说尤其是一个问题,他们无法合理地预测他们的代码是否会在使用线程的程序中使用。 当您阅读公共C运行库和标准C ++库实现的源代码时非常明显。 这样的代码往往充满了整个地方的小锁,以确保代码在线程中使用时仍能正常运行。 即使您没有以线程方式实际使用该代码,也要为此付出代价。 一个很好的例子是std :: shared_ptr <>。 您支付引用计数的原子更新,即使智能指针仅在一个线程中使用过。 并且标准没有提供要求非原子更新的方法,添加该功能的提议被拒绝。

并且它在其他方面也是有害的,编译器无法确保您自己的代码是线程安全的。 完全取决于你的线程安全性。 很难做到,而且这种方法一直存在微妙且非常难以诊断的问题。

大问题,不易解决。 也许那是件好事,否则每个人都可能成为程序员;)

这是一个很好的问题,即使它相当广泛,我期待着听到专家的意见。 我认为@JimCownie在以下讨论中对此有一个很好的评论omp_set_num_threads(1)的原因比没有openmp慢

我认为自动矢量化和并行化通常是个问题。 如果在MSVC 2012中打开自动并行化(自动向量化是我的默认设置),它们似乎不能很好地混合在一起。 使用OpenMP似乎禁用了MSVC的自动矢量化。 对于使用OpenMP和自动矢量化的GCC,情况可能也是如此,但我不确定。

无论如何,我真的不相信编译器中的自动向量化。 一个原因是我不确定它是否循环展开以消除携带的循环依赖性以及标量代码。 出于这个原因,我尝试自己做这些事情。 我自己做矢量化(使用Agner Fog的矢量类),我自己展开循环。 通过手工完成这一点,我感到更加知己,我最大化所有并行性:TLP(例如使用OpenMP),ILP(例如通过使用循环展开删除数据依赖性)和SIMD(使用显式SSE / AVX代码)。

上面有很多好的信息,但正确的答案是在编译OpenMP时必须关闭一些优化。 一些编译器,如gcc,不这样做。

该答案结尾处的示例程序是在四个非重叠的整数范围中搜索值81。 它总是应该找到这个价值。 但是,在所有至少4.7.2的gcc版本中,程序有时不会以正确的答案终止。 要亲眼看看,请执行以下操作:

  • 将程序复制到文件parsearch.c
  • gcc -fopenmp -O2 parsearch.c编译它
  • 使用OMP_NUM_THREADS=2 ./a.out运行它
  • 再跑几次(也许10次),你会看到两个不同的答案

或者,您可以在不使用-O0情况下进行编译,并查看结果始终是正确的。

鉴于该程序没有竞争条件,编译器在-O2下的这种行为是不正确的。

行为是由全局变量globFound 。 请说服自己在预期的执行情况下, parallel for中的4个线程中只有一个parallel for写入该变量。 OpenMP语义定义如果全局(共享)变量仅由一个线程写入,则parallel-for之后的全局变量的值是该单个线程写入的值。 线程之间没有通过全局变量进行通信,因此不允许这样做,因为它会导致竞争条件。

编译器优化在-O2下的作用是它估计在循环中写入全局变量是昂贵的,因此将其缓存在寄存器中。 这发生在函数findit中,在优化之后,它将如下所示:

 int tempo = globFound ; for ( ... ) { if ( ...) { tempo = i; } globFound = tempo; 

但是使用这个“优化”代码,每个线程都会读取和写入globFound ,并且编译器本身会引入竞争条件。

编译器优化确实需要了解并行执行。 关于此的优秀材料由Hans-J出版。 Boehm,在内存一致性的一般主题下。

 #include  #define BIGVAL (100 * 1000 * 1000) int globFound ; void findit( int from, int to ) { int i ; for( i = from ; i < to ; i++ ) { if( i*i == 81L ) { globFound = i ; } } } int main( int argc, char *argv ) { int p ; globFound = -1 ; #pragma omp parallel for for( p = 0 ; p < 4 ; p++ ) { findit( p * BIGVAL, (p+1) * BIGVAL ) ; } if( globFound == -1 ) { printf( ">>>>NO 81 TODAY<<<<\n\n" ) ; } else { printf( "Found! N = %d\n\n", globFound ) ; } return 0 ; }