通过OpenMP SIMD进行256位矢量化会阻止编译器的优化(比如说函数内联)?

时间:2018-07-03 10:24:08

标签: c gcc openmp simd auto-vectorization

请考虑以下玩具示例,其中A是按列大顺序存储的n x 2矩阵,我想计算其列总和。 sum_0仅计算第一列的总和,而sum_1也计算第二列的总和。这实际上是一个人工示例,因为基本上不需要为此任务定义两个函数(我可以编写一个带有双循环嵌套的单个函数,其中外循环从0迭代到j) 。它旨在演示我实际遇到的模板问题。

/* "test.c" */
#include <stdlib.h>

// j can be 0 or 1
static inline void sum_template (size_t j, size_t n, double *A, double *c) {

  if (n == 0) return;
  size_t i;
  double *a = A, *b = A + n;
  double c0 = 0.0, c1 = 0.0;

  #pragma omp simd reduction (+: c0, c1) aligned (a, b: 32)
  for (i = 0; i < n; i++) {
    c0 += a[i];
    if (j > 0) c1 += b[i];
    }

  c[0] = c0;
  if (j > 0) c[1] = c1;

  }

#define macro_define_sum(FUN, j)            \
void FUN (size_t n, double *A, double *c) { \
  sum_template(j, n, A, c);                 \
  }

macro_define_sum(sum_0, 0)
macro_define_sum(sum_1, 1)

如果我用

进行编译
gcc -O2 -mavx test.c

GCC(例如最新的8.2)在进行内联,持续传播和消除无效代码后,将针对功能c1Check it on Godbolt)优化出涉及sum_0的代码。

我喜欢这个把戏。通过编写单个模板函数并传入不同的配置参数,优化的编译器可以生成不同的版本。与复制和粘贴大部分代码并手动定义不同的功能版本相比,它要干净得多。

但是,如果我使用以下命令激活OpenMP 4.0+,则会失去这种便利性

gcc -O2 -mavx -fopenmp test.c

sum_template不再被内联,并且不应用任何无效代码消除(Check it on Godbolt)。但是,如果我删除标志-mavx以使用128位SIMD,则编译器优化将按我的期望进行工作(Check it on Godbolt)。那是一个错误吗?我正在使用x86-64(Sandybridge)。


备注

使用GCC的自动矢量化-ftree-vectorize -ffast-math不会出现此问题(Check it on Godbolt)。但是我希望使用OpenMP,因为它允许跨不同的编译器进行可移植的对齐方式编译。

背景

我为R包编写模块,该模块需要跨平台和编译器移植。编写R扩展名不需要Makefile。当R在平台上构建时,它知道该平台上的默认编译器,并配置一组默认编译标志。 R没有自动矢量化标志,但是有OpenMP标志。这意味着使用OpenMP SIMD是在R包中利用SIMD的理想方法。有关更多说明,请参见12

2 个答案:

答案 0 :(得分:2)

我迫切需要解决此问题,因为在我的真实C项目中,如果没有模板技巧用于自动生成不同功能版本(以下简称为“版本转换”),那么我总共需要编写1400行9种不同版本的代码,而不是单个模板的200行代码。

我能够找到一条出路,现在使用问题中的玩具示例发布解决方案。


我计划使用 inline 函数sum_template进行版本控制。如果成功,则在编译器执行优化时在编译时发生。但是,事实证明,OpenMP编译失败使此编译时版本控制失败。然后可以选择在预处理阶段仅使用 macros 进行版本控制。

要摆脱 inline 函数sum_template,我将其手动内联到宏macro_define_sum中:

#include <stdlib.h>

// j can be 0 or 1
#define macro_define_sum(FUN, j)                            \
void FUN (size_t n, double *A, double *c) {                 \
  if (n == 0) return;                                       \
  size_t i;                                                 \
  double *a = A, * b = A + n;                               \
  double c0 = 0.0, c1 = 0.0;                                \
  #pragma omp simd reduction (+: c0, c1) aligned (a, b: 32) \
  for (i = 0; i < n; i++) {                                 \
    c0 += a[i];                                             \
    if (j > 0) c1 += b[i];                                  \
    }                                                       \
  c[0] = c0;                                                \
  if (j > 0) c[1] = c1;                                     \
  }

macro_define_sum(sum_0, 0)
macro_define_sum(sum_1, 1)

在仅 macro 的此版本中,j在宏扩展过程中直接由0或1代替。而在问题的 inline 函数+ macro 方法中,我在预处理阶段只有sum_template(0, n, a, b, c)sum_template(1, n, a, b, c),而{{1 j正文中的}}仅在以后的编译时传播。

不幸的是,上述给出了错误。我无法在另一个宏中定义或测试宏(请参见123)。以sum_template开头的OpenMP杂项在这里引起问题。因此,我必须将此模板分为两部分:在编译指示之前的部分和在编译指示之后的部分。

#

我不再需要#include <stdlib.h> #define macro_before_pragma \ if (n == 0) return; \ size_t i; \ double *a = A, * b = A + n; \ double c0 = 0.0, c1 = 0.0; #define macro_after_pragma(j) \ for (i = 0; i < n; i++) { \ c0 += a[i]; \ if (j > 0) c1 += b[i]; \ } \ c[0] = c0; \ if (j > 0) c[1] = c1; void sum_0 (size_t n, double *A, double *c) { macro_before_pragma #pragma omp simd reduction (+: c0) aligned (a: 32) macro_after_pragma(0) } void sum_1 (size_t n, double *A, double *c) { macro_before_pragma #pragma omp simd reduction (+: c0, c1) aligned (a, b: 32) macro_after_pragma(1) } 。我可以使用定义的两个宏直接定义macro_define_sumsum_0。我也可以适当地调整编译指示。在这里,我没有使用模板功能,而是具有用于功能代码块的模板,并且可以轻松地重用它们。

在这种情况下,编译器的输出是预期的(Check it on Godbolt)。


更新

感谢各种反馈;它们都很有建设性(这就是为什么我喜欢Stack Overflow)。

感谢Marc Glisse,将我指向Using an openmp pragma inside #define。是的,没有搜索此问题对我来说是不好的。 sum_1是指令,而不是真正的宏,因此必须有某种方法将其放入宏中。这是使用#pragma运算符的简洁版本:

_Pragma

其他更改包括:

  • 我使用token concatenation生成函数名称;
  • /* "neat.c" */ #include <stdlib.h> // stringizing: https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html #define str(s) #s // j can be 0 or 1 #define macro_define_sum(j, alignment) \ void sum_ ## j (size_t n, double *A, double *c) { \ if (n == 0) return; \ size_t i; \ double *a = A, * b = A + n; \ double c0 = 0.0, c1 = 0.0; \ _Pragma(str(omp simd reduction (+: c0, c1) aligned (a, b: alignment))) \ for (i = 0; i < n; i++) { \ c0 += a[i]; \ if (j > 0) c1 += b[i]; \ } \ c[0] = c0; \ if (j > 0) c[1] = c1; \ } macro_define_sum(0, 32) macro_define_sum(1, 32) 设为宏参数。对于AVX,值32表示对齐良好,而值8(alignment)本质上表示没有对齐。需要Stringizing将这些标记解析为sizeof(double)需要的字符串。

使用_Pragma检查预处理结果。编译会提供所需的程序集输出(Check it on Godbolt)。


关于Peter Cordes信息丰富的答案的一些评论

使用编译器的函数属性。我不是专业的C程序员。我对C的经验仅来自编写R扩展。开发环境确定我对编译器属性不是很熟悉。我知道一些,但并不真正使用它们。

gcc -E neat.c在我的应用程序中不是问题,因为我将分配对齐的内存并应用填充以确保对齐。我只需要向编译器承诺对齐方式,以便它可以生成对齐的加载/存储指令。我确实需要对未对齐的数据进行一些矢量化处理,但这仅占整个计算的非常有限的一部分。即使我因分割未对齐负载而导致性能下降,也不会在现实中注意到。我也不用自动矢量化编译每个C文件。我仅在L1缓存上的操作热时执行SIMD(即,它是CPU绑定而不是内存绑定)。顺便说一句,-mavx256-split-unaligned-load用于 GCC ;对于其他编译器有什么作用?

我知道-mavx256-split-unaligned-loadstatic inline之间的区别。如果只有一个文件访问inline函数,我将其声明为inline,以便编译器不会生成它的副本。

即使没有 GCC static,OpenMP SIMD也可以有效地进行还原。但是,在精简结束时,它不使用水平加法来累加累加器寄存器内的结果;它运行一个标量循环以将每个双字加起来(请参见Godbolt output中的代码块.L5和.L27)。

吞吐量是一个好点(特别是对于具有相对较大的延迟但具有高吞吐量的浮点运算)。应用SIMD的真正C代码是三重循环嵌套。我展开外部两个循环以扩大最内部循环中的代码块,以提高吞吐量。那么最里面的一个的矢量化就足够了。在这个“问题与解答”中的玩具示例中,我只是对一个数组求和,我可以使用-ffast-math GCC 进行循环展开,并使用多个累加器来提高吞吐量。


关于此问答

我认为大多数人会以比我更多的技术方式来处理此问与答。他们可能对使用编译器属性或调整编译器标志/参数以强制函数内联感兴趣。因此,彼得的答案以及马克在该答案下的评论仍然非常有价值。再次感谢。

答案 1 :(得分:2)

解决此问题的最简单方法是使用__attribute__((always_inline)) 或其他特定于编译器的替代。

#ifdef __GNUC__
#define ALWAYS_INLINE __attribute__((always_inline)) inline
#elif defined(_MSC_VER)
#define ALWAYS_INLINE __forceinline inline
#else
#define ALWAYS_INLINE  inline  // cross your fingers
#endif


ALWAYS_INLINE
static inline void sum_template (size_t j, size_t n, double *A, double *c) {
 ...
}

Godbolt proof that it works.

此外,不要忘记使用-mtune=haswell,而不仅仅是-mavx。通常是个好主意。 (但是,有前途的对齐数据将阻止gcc的默认-mavx256-split-unaligned-load调整将256位负载拆分为128位vmovupd + vinsertf128,因此 this 的代码生成tune = haswell可以很好地使用该函数,但是通常情况下,您希望gcc可以自动矢量化其他函数。

您真的不需要staticinline一起使用;如果编译器决定不内联它,则它至少可以在编译单元之间共享相同的定义。


通常,gcc根据函数大小试探法决定是否内联。但是即使设置-finline-limit=90000也不会使gcc与您的#pragma ompHow do I force gcc to inline a function?)内联。我一直在猜测gcc并没有意识到内联后的常量传播会简化条件,但是90000个“伪指令”似乎很大。可能还有其他启发式方法。

可能,OpenMP对每个功能的设置方式不同,如果使优化器内联到其他功能中,可能会破坏优化器。使用__attribute__((target("avx")))可以阻止该函数内联到不使用AVX编译的函数中(因此,您可以安全地进行运行时调度,而不必在整个if(avx)条件下使用AVX指令来内联“感染”其他函数。)

OpenMP不能通过常规自动矢量化获得的一件事就是无需启用-ffast-math就可以对减少量进行矢量化。

不幸的是,OpenMP仍然没有费心地使用多个累加器进行展开或隐藏FP延迟的任何操作。 #pragma omp很好地暗示了一个循环实际上很热,值得花代码大小,因此即使没有-fprofile-use,gcc也应该这样做。

因此,特别是如果它曾经在L2或L1高速缓存(或可能是L3)中很热的数据上运行,则应该做一些事情以获得更好的吞吐量。

顺便说一句,对齐对于Haswell的AVX通常不是什么大问题。但实际上,对于SKX上的AVX512,64字节对齐确实要重要得多。对于未对齐的数据,速度可能会降低20%,而不是几个百分点。

(但是在编译时承诺对齐与在运行时实际对齐数据是一个单独的问题。这都很有帮助,但是在gcc7及更早版本(或任何不带AVX的编译器)上,希望在编译时进行对齐会使代码更紧密。)< / p>