【问题标题】:Why gcc autovectorization does not work on convolution matrix biger than 3x3?为什么 gcc 自动矢量化不适用于大于 3x3 的卷积矩阵?
【发布时间】:2017-04-19 06:53:11
【问题描述】:

我已经为卷积矩阵实现了以下程序

#include <stdio.h>
#include <time.h>

#define NUM_LOOP 1000
#define N 128   //input or output dimention 1
#define M N     //input or output dimention 2
#define P 5 //convolution matrix dimention 1 if you want a 3x3 convolution matrix it must be 3
#define Q P     //convolution matrix dimention 2
#define Csize P*Q   
#define Cdiv  1     //div for filter 
#define Coffset 0   //offset 

//functions
void unusual(); //unusual implementation of convolution
void naive();
//data
unsigned short int input[N][M] __attribute__(( aligned(32))); // input data
unsigned short int output[N][M] __attribute__(( aligned(32))); // out put data
unsigned short int kernel[P][Q] __attribute__(( aligned(32)));//convolution coefficients

int main(){
    struct timespec tStart, tEnd;//used to record the processiing time
    double tTotal , tBest=10000;//minimum of toltal time will asign to the best time

    int w=0;
    do{// this loop repeat the body to record the best time
        clock_gettime(CLOCK_MONOTONIC,&tStart);

        //function to be executed here :

        unusual();

        clock_gettime(CLOCK_MONOTONIC,&tEnd);
        tTotal = (tEnd.tv_sec - tStart.tv_sec);
        tTotal += (tEnd.tv_nsec - tStart.tv_nsec) / 1000000000.0;

        if(tTotal<tBest)
            tBest=tTotal;
    } while(w++ < NUM_LOOP);

    printf(" The best time: %lf sec in %d repetition for %dX%d matrix\n",tBest,w, MAX1, MAX2);

    return 0;
}

//unusual sequential convolution
void unusual(){
    int i, j,k,temp;

    for (i=P/2; i< N-P/2; i++){
        for(j=Q/2; j< M-Q/2; j++){
            temp=0;
            for(k=0; k< Csize; k++){
                temp += (kernel[k/P][k%Q]) * (input[i - (P/2) + (k/Q)][j - (Q/2) + (k%Q)]);

            }
            output[i][j]=((temp/(Cdiv))+Coffset);
        }
    }
}
//The naive implementation
inline void naive(){
    int i, j,k,l,temp;
    for (i=P/2; i< N-P/2; i++){
        for(j=Q/2; j< M-Q/2; j++){
            temp=0;

            for(k = 0; k <  P; k++){ 
                for(l = 0; l <  Q; l++){
                    temp += (kernel[k][l]) * (input[i - (P/2)+k][j - (Q/2)+l]);
                }
            }
            output[i][j]=((temp/(Cdiv))+Coffset);
        }
    }
}

问题是当我使用 -O3 进行自动矢量化时,它只适用于 3x3 卷积矩阵。我已经看到组装输出和自动矢量化只是对 3x3 内核进行了一些更改并合理地提高了性能(快 20 倍注意:不寻常 func 的标量版本比 naive fun 慢)但 5x5 卷积矩阵没有任何改进

更新:我在问题中添加了简单的实现,并将图片大小更改为 NxM,将卷积矩阵更改为内核,将 Cdim1xCdim2 更改为 PxQ,并将 seqConv 函数更改为不寻常的澄清。问题不在于改进异常功能的实现。问题是虽然所有元素都在内存的相同位置,但 gcc 使用启发式等。为什么 gcc 未能改进这种不寻常的实现? 注意:问题不在于简单的实现。 gcc -O3 将 3x3、5x5 内核的简单实现提高了约 7 倍的速度。它也适用于 7x7 和 9x9 加速约 1.5。为了改进卷积,我使用了内在函数,并且加速比简单的实现提高了 40 倍以上,比不寻常的卷积快了 2 倍。所以我的矢量化比我不寻常的矢量化快约 80 倍。手动调整优化不是问题。自动矢量化优化是问题所在,也是失败的原因。

GCC 命令:gcc -Wall -march=native -O3 -o "%e" "%f"

平台:Linux mint、Skylake、gcc 6.2

提前致谢

【问题讨论】:

  • 你能完成这个足以让它编译吗?
  • 将矩阵放在单个寄存器中听起来并不是很有用,因为它是二维的,所以需要一些疯狂的洗牌才能将数据与它对齐。但我会再次经历那一堆乱七八糟的 asm,看看它做了什么
  • conv 的每个元素使用一个向量,因此向量大小不是问题。寄存器的数量可能仍然存在,但我真的不确定。当然,它不能使用 25 个向量来保存 5x5 情况下的条目,但它可以即时广播一些。虽然 GCC 一直不合作,但我花了很多时间试图说服它为 5x5 编写漂亮的代码,但它只是没有,甚至没有内在函数。好吧,也许如果我变得非常明确并且没有给优化器留下任何东西,但是我必须手动展开并失去一般性。
  • 此外,GCC 正在使用 dwords 进行所有数学运算,这是不必要的(如果 Cdiv 停止为 1,除了某些特定情况和四舍五入,它肯定必须是至少是 2 的幂,因为没有整数向量除法)。所以short temp 会产生更好的代码。不过,对于 5x5 来说,这显然还是太高了。 GCC 也非常小心不要覆盖填充,所以每一行都以 15 个标量卷积开始和结束,这很容易用内在函数避免(顶部和底部应该仍然有,否则读取将超出输入)。跨度>
  • 看来你不是第一个研究这个问题的人,Auto vactorization 非常复杂,工作(或不工作)很大程度上取决于你如何编写代码。重写你的代码结构可能会有很大帮助,我相信链接页面并不总是在 stackoverflow 上进行,但我想将你链接到以下页面:locklessinc.com/articles/vectorize 这家伙在 vactorization 中做了很多工作,我希望它可以帮助您解决问题。

标签: c gcc x86 compiler-optimization auto-vectorization


【解决方案1】:

我的猜测是由于内存对齐问题而无法优化。您已将卷积指定为 2 字节短裤。大多数 SSE 函数喜欢使用 128 位向量,而 AVX 喜欢使用 512 位向量。

在我的机器上我这样声明 conv:

uint16_t conv[Cdim1][8] = {0}; //You need to pad extra fields with zeroes

然后像这样替换内部循环:

for(ki = 0; ki < Cdim; ++ki) 
    for(kj = 0; kj < 8; ++kj)
        temp += (conv[ki][kj]) * (input[i - (Cdim1/2) + ki][j - (Cdim2/2) + kj]);

编译:gcc so.c -Wall -Wextra -Ofast -mtune=native 给了我向量优化!

坏事:

  • 不要使用 8。尝试找到所需的最小填充并制作宏,使其适用于维度 >= 8 的卷积矩阵
  • 用一些零填充输入,以便最后未定义的行为消失
  • 请注意,这实际上并没有提高您的性能。事实上,它的运行速度较慢!
  • 请注意,如果您进一步修改它以按照以下顺序执行循环 for(ki) for(i) for(j) for(kj),则可以压缩几个循环。这可能是由于寄存器压力较小,因为每行 conv 可以存储更长的时间。这也可能是我的 CPU 出现故障。
  • 您可能还想在声明变量时考虑使用__attribute__ ((aligned (8)))。在这种情况下,它没有改变任何东西,但是在优化时你也想考虑这一点。当然,这仅适用于 GCC,您将需要其他针对 MSVC 的 hack。

【讨论】:

  • 谢谢,但是你改变了内部循环。正如我在 cmets 中提到的,我会更新这个问题。 gcc 可以向量化四个循环实现。我的 gcc 6.2 将其矢量化为 3x3 和 5x5 内核,速度提升约 6 倍。甚至 gcc 也提高了 7x7 和 9x9 。顺便说一句,问题在于不寻常的实现,称为seqconv,而不是天真的实现。
【解决方案2】:

似乎没有人有兴趣回答这个问题。所以我会分享我的发现并在未来更新我的答案。

第一次更新: 根据我的经验,gcc -fopt-info-vec 报告矢量化为Csize &lt;= 16 这是因为矢量化因子是16 并且它是gcc 不矢量化的原因之一其他内核大小的不寻常实现。向量化因子是指可以放入向量中的元素的数量。在这种情况下,short integer 等于 16-bit 元素。

来自wikipedia:

在第一步中,编译器会寻找可以阻止向量化的障碍。矢量化的一个主要障碍是真正的数据依赖性比矢量长度短。其他障碍包括函数调用和较短的迭代次数。

【讨论】:

    【解决方案3】:

    自动矢量化的主要障碍是非常量循环变体。在您的实现中,如果您使用 int Csize = P*Q; 它不会被矢量化。因此,为了帮助自动矢量,您应该考虑这一点。这不是问题,因为您声明了Csize 就像#define Csize。但是在你的作品中注意它。 然后,您不寻常的实现是对中性实现的循环转换,这是编译器中的一种优化方法。看来你毁了天真的实现。您的发现表明由于 16 而受到限制,所以我展开了您不寻常的功能,自动矢量化器说它已被矢量化。

    for(k=0; k< P*Q; k+=2){
                    temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + (k/Q)][j - (Q/2) + (k%Q)]);
                    temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + ((k+1)/Q)][j - (Q/2) + ((k+1)%Q)]);
    }
    

    它也适用于 7x7 内核:

    for(k=0; k< P*Q; k+=4){//IACA_START
                    temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + (k/Q)][j - (Q/2) + (k%Q)]);
                    temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + ((k+1)/Q)][j - (Q/2) + ((k+1)%Q)]);
                    temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + ((k+2)/Q)][j - (Q/2) + ((k+2)%Q)]);
                    temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + ((k+3)/Q)][j - (Q/2) + ((k+3)%Q)]);
    }
    

    您不需要自己展开它,您也许可以通过#pragma 属性强制编译器展开或更改循环结构。这是因为编译器使用SLP 概念进行自动矢量化,有趣的是SLP 是基于展开!。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-12-29
      • 1970-01-01
      • 2011-06-28
      • 1970-01-01
      • 2019-03-27
      • 1970-01-01
      • 2018-12-16
      相关资源
      最近更新 更多