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

时间:2014-11-13 23:47:39

标签: c optimization fortran vectorization avx

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

问题最容易通过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。

显然我可以使用内在函数或其他任何东西来编写这个,但是有可能让编译器以上述方式自动向量化,通过&#34; normal&#34;源代码?

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

3 个答案:

答案 0 :(得分: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;
    }

它将使您能够使用函数调用来向量化循环而无需内联,因此无需生成大量代码。 (我没有详细探索你的代码片段;而是以文本形式回答了你提出的问题)

答案 1 :(得分:0)

想到的直接问题是输入/输出指针缺少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]);
    }    
}

可能可以进行矢量化。我认为它不会那么快,以记忆速度,但你永远不会知道,直到你的基准。

答案 2 :(得分:0)

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

for (int i = 0; i < len; i++)
{
    lots_of_code_a(i, in_arr1);
}