使用OpenMP会阻止GCC自动向量化

6

我一直在努力让我的代码能够被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有自己处理向量化的方法吗?我需要明确告诉它吗?


我认为你可以在这个问题的答案中找到有意义的信息。 - Massimiliano
谢谢,这解释了如何在OpenMP中使用SIMD,但似乎没有解释为什么当我使用OpenMP时,已经工作的SIMD实现停止工作。难道不能同时使用吗? - superbriggs
1
这也意味着我只能在相同数量的位上操作,它们只是分布在不同的数字之间。在使用GCC时,我没有被问到要将多少位拆分到寄存器中。由于我正在使用一台大学“超级计算机”,我假设硬件有额外的空间用于SIMD。我该如何找出是否正确? - superbriggs
硬件是AMD处理器,将使用3Dnow! - superbriggs
我的问题是,由于硬件确实有特定的寄存器可以容纳更多内容以帮助向量化,那么在使用GCC时,如何做到这一点,考虑到该链接中提供的函数会将正常大小的寄存器分成块。 - superbriggs
4个回答

9

在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;

在GCC 4.7之前的向量化器无法对该循环进行向量化处理。这不是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代码时相同。
其原因在于向量化器无法证明par->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可以消除无法证明的非别名问题,随后的OpenMP代码可以被GCC 4.6.1向量化。
#pragma omp parallel for schedule(static)
for (int i = 0; i < N; i++)
   a[i] = b[i] + c[i];

非常好的回答。但是你能再多说一些关于“这只是为了说明情况 - GCC使用了稍微不同的公式”吗?GCC使用什么公式? - Z boson
@Zboson,我可以将它粘贴在这里(丑陋的),但是您最好运行gcc -fdump-tree-all -fopenmp foo.c并在OpenMP扩展后自行检查AST,通常位于foo.c.015t.ompexp中。 不同之处在于GCC通过向前r个线程中的第一个线程提供一个额外的迭代来分配剩余部分 r = N%num_threads 的结果。 - Hristo Iliev

3

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

  1. OpenMP是否有处理向量化的特殊方法?

是的...但是仅限于即将推出的OpenMP 4.0版本。上面提供的链接很好地解释了这个构造。然而,当前的OpenMP 3.1版本并不“意识到”SIMD的概念。因此,在实践中(或者至少在我的经验中),每当在循环中使用openmp工作共享构造时,自动向量化机制就会被禁用。无论如何,这两个概念是正交的,你仍然可以从中受益(请参见另一个答案)。

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

恐怕是的,至少目前是这样。我建议重写考虑中的循环,以使向量化变得明确(例如,在英特尔平台上使用内部函数,在IBM上使用Altivec等)。


非常感谢。您提供的第一个链接给出了函数“VECTOR_ADD”。我已经了解到它使用一个普通大小的寄存器,因此只允许对小数字进行向量化。我知道我的硬件有专门的寄存器来处理SIMD,这样就不会发生这种情况。有没有办法使OpenMP使用这个寄存器?在GCC为我完成所有工作之前,我需要使用这些函数吗?我不明白为什么OpenMP会阻止这种形式的工作。您提供的第二个链接说它们可以一起使用,但是没有说明我应该如何实现。再次非常感谢。 - superbriggs
主要思想是OpenMP不能意识到SIMD化,因为你在VECTOR_ADD中负责它。我从未使用过3Dnow,但在英特尔平台上,可以使用intrinsics来显式地向量化代码。主要缺点是要么失去可移植性(因为intrinsics在其他平台上无法工作),要么失去可读性/可维护性(因为有条件的编译)。 - Massimiliano
对于这个项目,可维护性和可移植性并不重要。我目前没有使用VECTOR_ADD,只是以一种GCC可以看到发生了什么并自动矢量化的方式将其放在一个循环中。 - superbriggs

1
您正在询问“为什么启用OpenMP后GCC无法进行矢量化?”。
看起来这可能是GCC的错误 :) http://gcc.gnu.org/bugzilla/show_bug.cgi?id=46032 否则,OpenMP API可能会引入依赖项(控制或数据),从而防止自动矢量化。要自动矢量化,给定代码必须没有数据/控制依赖性。使用OpenMP可能会导致一些虚假的依赖关系。
注意:OpenMP(4.0之前)是使用线程级并行处理的,这与SIMD /矢量化是正交的。程序可以同时使用OpenMP和SIMD并行处理。

1
我在搜索有关gcc 4.9选项openmp-simd的评论时遇到了这篇文章,该选项应激活OpenMP 4 #pragma omp simd,而不激活omp parallel(线程)。gcc bugzilla pr60117(已确认)显示了一种情况,其中#pragma omp防止了自动向量化,而没有#pragma则会出现自动向量化。
即使使用simd子句,gcc也不会对omp parallel for进行矢量化(并行区域只能自动矢量化嵌套在parallel for下的内部循环)。我不知道除icc 14.0.2之外还有哪个编译器可以推荐实现#pragma omp parallel for simd;对于其他编译器,需要使用SSE intrinsics编码才能获得此效果。
在我的测试中,Microsoft编译器不会在并行区域内执行任何自动矢量化,这表明gcc在这种情况下具有明显的优势。
将单个循环的并行化和矢量化结合起来,即使使用最佳实现,也会遇到几个困难。我很少看到通过添加矢量化到并行循环中获得两倍或三倍的速度提升。例如使用AVX双数据类型进行矢量化,可以有效地将块大小缩小4倍。典型实现仅在整个数组对齐的情况下可以实现数据块对齐,而且块还必须是矢量宽度的精确倍数。当块不全对齐时,由于不同的对齐方式存在固有的工作不平衡。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接