让编译器以合理的方式自动矢量化代码

Posted

技术标签:

【中文标题】让编译器以合理的方式自动矢量化代码【英文标题】:Getting the compiler to auto-vectorize code in a sensible manner 【发布时间】:2014-11-13 23:47:39 【问题描述】:

我正试图弄清楚如何构造数值模拟的主循环代码,以便编译器以紧凑的方式生成很好的矢量化指令。

这个问题最容易用 C 伪代码来解释,但我也有一个受相同问题影响的 Fortran 版本。考虑以下循环,其中lots_of_code_* 是一些复杂的表达式,它们会产生相当多的机器指令。

void process(const double *in_arr, double *out_arr, int len)

    for (int i = 0; i < len; i++)
    
        const double a = lots_of_code_a(i, in_arr);
        const double b = lots_of_code_b(i, in_arr);
        ...
        const double z = lots_of_code_z(i, in_arr);

        out_arr[i] = final_expr(a, b, ..., z);
    

当使用 AVX 目标编译时,英特尔编译器生成的代码类似于

process:
    AVX_loop
    AVX_code_a
    AVX_code_b
    ...
    AVX_code_z
    AVX_final_expr
    ...
    SSE_loop
    SSE_instructions
    ...
    scalar_loop
    scalar_instructions
    ...

生成的二进制文件已经相当大了。不过,我的实际计算循环看起来更像以下内容:

void process(const double *in_arr1, ... , const double *in_arr30, 
             double *out_arr1, ... double *out_arr30,
             int len) 

    for (int i = 0; i < len; i++)
    
        const double a1 = lots_of_code_a(i, in_arr1);
        ...
        const double a30 = lots_of_code_a(i, in_arr30);

        const double b1 = lots_of_code_b(i, in_arr1);
        ...
        const double b30 = lots_of_code_b(i, in_arr30);

        ...
        ...

        const double z1 = lots_of_code_z(i, in_arr1);
        ...
        const double z30 = lots_of_code_z(i, in_arr30);

        out_arr1[i] = final_expr1(a1, ..., z1);
        ...
        out_arr30[i] = final_expr30(a30, ..., z30);
    

这确实导致了一个非常大的二进制文件(Fortran 版本为 400KB,C99 为 800KB)。如果我现在将lots_of_code_* 定义为函数,那么每个函数都会变成非向量化代码。每当编译器决定内联一个函数时,它都会对其进行矢量化,但似乎每次也会复制代码。

在我看来,理想的代码应该是这样的:

AVX_lots_of_code_a:
    AVX_code_a
AVX_lots_of_code_b:
    AVX_code_b
...
AVX_lots_of_code_z:
    AVX_code_z
SSE_lots_of_code_a:
    SSE_code_a
...
scalar_lots_of_code_a:
    scalar_code_a
...
...
process:
    AVX_loop
    call AVX_lots_of_code_a
    call AVX_lots_of_code_a
    ...
    SSE_loop
    call SSE_lots_of_code_a
    call SSE_lots_of_code_a
    ...
    scalar_loop
    call scalar_lots_of_code_a
    call scalar_lots_of_code_a
    ...

这显然会导致代码更小,但仍然与完全内联的版本一样优化。如果运气好的话,它甚至可能适合 L1。

显然,我可以使用内在函数或其他方法自己编写此代码,但是否可以通过“正常”源代码以上述方式自动矢量化编译器?

我知道编译器可能永远不会为函数的每个矢量化版本生成单独的符号,但我认为它仍然可以在 process 内内联每个函数一次并使用内部跳转来重复相同的代码块,而不是为每个输入数组复制代码。

【问题讨论】:

这个问题让我反击:为什么处理器内在函数在那里? AFAIK 所以编码器可以做编译器太笨的优化。 嗯,关键是编译器为每个表达式生成了不错的代码。如果我坐下来用内在函数写出所有这些表达式,这将花费很长时间,并且可能不会像编译器所做的那样好。它正在做的唯一愚蠢的事情就是重复自己...... 明白,说得好。 ...更不用说我必须分别编写每个和所有表达式的 AVX、SSE 和标量版本。然后还必须自己为process 编写剩余循环。实在不行。 @St0fF 内在函数是为那些认为自己可以比编译器做得更好并愿意投资于它的人而存在的。到今天为止,如果您知道自己在做什么,那么在矢量化方面击败编译器仍然很容易。当然,如果你在乎的话,你仍然需要投资。 【参考方案1】:

像你这样的问题的正式回答:

考虑使用支持 OpenMP4.0 SIMD(我没有说内联)的函数或等效的专有机制。在英特尔编译器或新的 GCC4.9 中可用。

在此处查看更多详细信息:https://software.intel.com/en-us/node/522650

例子:

//Invoke this function from vectorized loop
#pragma omp declare simd
    int vfun(int x, int y)
    
        return x*x+y*y;
    

它将使您能够使用函数调用向量化循环,而无需内联,因此无需大量代码生成。 (我并没有真正详细研究您的代码 sn-p;而是以文本形式回答了您提出的问题)

【讨论】:

谢谢,我会调查的。快速浏览一下文档表明它主要用于逐点标量函数,因此编译器可能会阻塞诸如微分例程之类的事情(但是如果不进行一些内存收集,其中很多都无法矢量化..) 事实上SIMD功能的适用范围非常广泛。看一下启用 SIMD 的函数示例,其中函数内部有 while 循环:extremecomputingtraining.anl.gov/files/2014/08/… 的最后 4 个箔。或者只是探索更多关于#pragma omp declare simd 子句的信息,例如“uniform”、“linear”等。 WRT 你的收集/分散点 - 我不确定什么是关系。是的:如果您的步幅基本上不规则并且使用足够宽的 ISA - 那么您的性能会更差,如果您使用编译器或内在函数或汇编,则没有太大差异(通常情况下,手动编码的水平总和对 AVX 没有意义- 512)。另一方面,它与启用 SIMD 的函数主题不太相关,不,不规则步幅的存在不会阻止循环和函数以某种方式(或多或少)被 OpenMP4.0 编译器矢量化。【参考方案2】:

想到的直接问题是输入/输出指针上缺少restrict。输入是const,所以可能不是太大的问题,除非你有多个输出指针。 除此之外,我推荐 -fassociative-math 或任何 ICC 等效项。从结构上讲,您似乎在遍历数组,在数组上执行多个独立操作,这些操作最终只会组合在一起。严格的 fp 合规性可能会在数组操作上杀死你。最后,如果你需要比 vector_registers - input_arrays 更多的中间结果,这可能不会被矢量化。编辑: 我想我现在看到了你的问题。您对不同的数据调用相同的函数,并希望每个结果独立存储,对吗?问题是相同的函数总是写入相同的输出寄存器,因此,随后的矢量化调用会破坏早期的结果。解决方案可能是:每次都会推送的结果堆栈(在内存中或像旧的 x87 FPU 堆栈一样)。如果在内存中,它很慢,如果是 x87,它不是矢量化的。坏主意。 有效地将多个函数写入不同的寄存器。代码重复。坏主意。旋转寄存器,就像在 Itanium 上一样。你没有安腾?您并不孤单。这可能无法在当前架构上轻松矢量化。对不起。 编辑,你显然没问题:

void function1(double const *restrict inarr1, double const *restrict inarr2, \
               double *restrict outarr, size_t n)

  for (size_t i = 0; i<n; i++)
    
      double intermediateres[NUMFUNCS];
      double * rescursor = intermediateres;
      *rescursor++ = mungefunc1(inarr1[i]);
      *rescursor++ = mungefunc1(inarr2[i]);
      *rescursor++ = mungefunc2(inarr1[i]);
      *rescursor++ = mungefunc2(inarr2[i]);
      ...
      outarr[i] = finalmunge(intermediateres[0],...,intermediateres[NUMFUNCS-1]);
        

可能是可矢量化的。我认为它不会那么快,以内存速度运行,但在你进行基准测试之前你永远不会知道。

【讨论】:

实际代码确实使用了restrict,但为了清楚起见,我把它留在这里。无论如何,这对于 Fortran 版本来说都不是问题。我会调查-fassociative-math。我暂时真的不需要严格的fp模式。你能否澄清关于中间结果的最后一点?谢谢! 老实说,很难说出您的问题出在哪里。需要更多的上下文。有很多问题会影响自动矢量化。除非内联,否则函数调用将阻止向量化,对齐会导致可笑的代码膨胀到向量化不再有利可图的地步……你明白了。 这里的问题不在于代码没有矢量化:没有函数调用的版本看起来被编译器很好地矢量化了。正如您所说,只要调用内联,版本 with 函数调用就会被矢量化。关键是当我多次重复 same 调用时,编译器要么单独内联 each 调用(巨大),要么对非向量化代码进行外部调用(慢) .就上下文而言,问题是与时间相关的 PDE 系统。大多数(但不是全部)重复调用是针对各个方向的有限差分导数。 关于您的编辑:在我们的特殊情况下,我们需要如此多的中间值,无论如何都需要将事物推入堆栈,无论是否矢量化。 (在最坏的情况下,事情应该仍然适合 L3)所以关于寄存器需要被破坏的点根本不是可以避免的。在其他条件相同的情况下,使用向量加法和向量乘法应该仍然比标量加法和标量乘法快一些。 如果你打算使用-fassociative-math,你不妨改用-Ofast【参考方案3】:

如果您将lots_of_code 块移动到没有for 循环的单独编译单元中,它们可能不会进行vecorize。除非编译器有向量化的动机,否则它不会向量化代码,因为向量化可能会导致管道中的延迟更长。为了解决这个问题,将循环分成 30 个循环,并将每个循环放在一个单独的编译单元中,如下所示:

for (int i = 0; i < len; i++)

    lots_of_code_a(i, in_arr1);

【讨论】:

以上是关于让编译器以合理的方式自动矢量化代码的主要内容,如果未能解决你的问题,请参考以下文章

static

CMake如何验证循环是不是自动矢量化

自动矢量化随机播放指令

gcc中的数组与指针自动矢量化

在 C 和 C++ 中对齐堆数组以简化编译器 (GCC) 向量化

为啥库需要硬编码矢量化而不是编译器自动矢量化