使用OpenMP停止GCC自动矢量化

我一直在努力使我的代码能够被GCC自动矢量化,但是,当我包含-fopenmp标志时,它似乎停止了所有自动矢量化的尝试。 我正在使用ftree-vectorize -ftree-vectorizer-verbose=5进行矢量化和监控。

如果我不包含该标志,它会开始向我提供有关每个循环的大量信息,如果它是矢量化的,为什么不。 当我尝试使用omp_get_wtime()函数时,编译器停止,因为它无法链接。 一旦包含了标志,它就会简单地列出每个函数并告诉我它中的向量化0循环。

我已经阅读了其他一些地方已经提到这个问题,但他们并没有真正找到任何解决方案: http://software.intel.com/en-us/forums/topic/295858 http:// gcc。 gnu.org/bugzilla/show_bug.cgi?id=46032 。 OpenMP有自己的处理矢量化的方法吗? 我需要明确告诉它吗?

GCC矢量化器存在一个缺点,似乎已在最近的GCC版本中得到解决。 在我的测试用例中,GCC 4.7.2成功地引导了以下简单循环:

 #pragma omp parallel for schedule(static) for (int i = 0; i < N; i++) a[i] = b[i] + c[i] * d; 

同时,GCC 4.6.1没有抱怨,循环包含无法分析的函数调用或数据引用。 矢量化器中的错误由GCC实现parallel for循环的方式触发。 处理和扩展OpenMP构造时,简单的循环代码转换为类似于此的东西:

 struct omp_fn_0_s { int N; double *a; double *b; double *c; double d; }; void omp_fn_0(struct omp_fn_0_s *data) { int start, end; int nthreads = omp_get_num_threads(); int threadid = omp_get_thread_num(); // This is just to illustrate the case - GCC uses a bit different formulas start = (data->N * threadid) / nthreads; end = (data->N * (threadid+1)) / nthreads; for (int i = start; i < end; i++) data->a[i] = data->b[i] + data->c[i] * data->d; } ... struct omp_fn_0_s omp_data_o; omp_data_o.N = N; omp_data_o.a = a; omp_data_o.b = b; omp_data_o.c = c; omp_data_o.d = d; GOMP_parallel_start(omp_fn_0, &omp_data_o, 0); omp_fn_0(&omp_data_o); GOMP_parallel_end(); N = omp_data_o.N; a = omp_data_o.a; b = omp_data_o.b; c = omp_data_o.c; d = omp_data_o.d; 

在4.7之前的GCC中的矢量化器无法矢量化该循环。 这不是特定于OpenMP的问题。 人们可以很容易地重现它,完全没有OpenMP代码。 为了证实这一点,我写了以下简单的测试:

 struct fun_s { double *restrict a; double *restrict b; double *restrict c; double d; int n; }; void fun1(double *restrict a, double *restrict b, double *restrict c, double d, int n) { int i; for (i = 0; i < n; i++) a[i] = b[i] + c[i] * d; } void fun2(struct fun_s *par) { int i; for (i = 0; i < par->n; i++) par->a[i] = par->b[i] + par->c[i] * par->d; } 

可以预期两个代码(通知 - 这里没有OpenMP!)应该同样好地矢量化,因为用于指定不会发生混叠的restrict关键字。 不幸的是,GCC <4.7不是这种情况 - 它成功地在fun1对循环进行了矢量化,但是在fun2引用它,原因与它编译OpenMP代码时的原因相同。

这样做的原因是矢量化器无法certificatepar->d不在par->apar->bpar->c指向的内存中。 fun1并非总是如此,其中有两种情况可能:

  • d作为值参数传递给寄存器;
  • d作为堆栈的值参数传递。

在x64系统上,System V ABI要求在XMM寄存器(启用AVX的CPU上的YMM)中传递前几个浮点参数。 这就是d在这种情况下传递的方式,因此没有指针可以指向它 - 循环得到矢量化。 在x86系统上,ABI要求将参数传递到堆栈,因此d可能由三个指针中的任何一个别名。 实际上,如果指示使用-m32选项生成32位x86代码,GCC拒绝在fun1对循环进行矢量化。

GCC 4.7通过插入运行时检查来解决这个问题,这些检查确保dpar->d不会出现别名。

摆脱d删除了无法certificate的非别名,下面的OpenMP代码由GCC 4.6.1进行矢量化:

 #pragma omp parallel for schedule(static) for (int i = 0; i < N; i++) a[i] = b[i] + c[i]; 

我会尝试简要回答你的问题。

  1. OpenMP有自己的处理矢量化的方法吗?

是的……但是从传入的OpenMP 4.0开始。 上面发布的链接提供了对此构造的良好见解。 另一方面,当前的OpenMP 3.1并未“了解”SIMD概念。 因此,在实践中(或者,至少在我的经验中)发生的是,无论何时在循环上使用openmp工作共享构造,都禁止自动向量化机制。 无论如何,这两个概念是正交的,你仍然可以从两者中受益(参见其他答案 )。

  1. 我需要明确告诉它吗?

恐怕是的,至少目前是这样。 我将开始以一种使矢量化显式化的方式重写所考虑的循环(即我将在Intel平台上使用内在函数,在IBM上使用Altivec等等)。

您问“为什么GCC在启用OpenMP时无法进行矢量化?”。

看来这可能是GCC的一个bug 🙂 http://gcc.gnu.org/bugzilla/show_bug.cgi?id=46032

否则,OpenMP API可能会引入阻止自动向量化的依赖项(控件或数据)。 要自动转换,给定代码必须是数据/控件依赖性。 使用OpenMP可能会导致一些虚假依赖。

注意:OpenMP(4.0之前)是使用线程级并行,它与SIMD /向量化正交。 程序可以同时使用OpenMP和SIMD并行。

我在搜索有关gcc 4.9选项openmp-simd的评论时遇到了这篇文章,它应该激活OpenMP 4 #pragma omp simd而不激活omp parallel(线程)。 gcc bugzilla pr60117(已确认)显示了pragma omp阻止在没有编译指示的情况下发生的自动向量化的情况。

即使使用simd子句,gcc也不会向omp并行化向量化(并行区域只能自动向量化并行嵌套在并行区域内)。 我不知道除了icc 14.0.2之外的任何编译器,可以推荐用于实现#pragma omp parallel for simd; 与其他编译器一起,SSE内在函数编码将需要获得此效果。

在我的测试中,Microsoft编译器不会在并行区域内执行任何自动向量化,这显示了gcc在这种情况下的明显优势。

即使具有最佳实现,单个循环的组合并行化和向量化也存在若干困难。 通过向并行循环添加矢量化,我很少看到超过2倍或3倍的加速。 例如,使用AVX双数据类型的矢量化有效地将块大小减少了4倍。典型的实现只能在整个arrays对齐的情况下实现对齐的数据块,并且块也是矢量宽度的精确倍数。 当块未全部对齐时,由于变化的对齐,存在固有的工作不平衡。