为什么gcc autovectorization对3×3的卷积矩阵不起作用?

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

#include  #include  #define NUM_LOOP 1000 #define N 128 //input or output dimention 1 #define MN //input or output dimention 2 #define P 5 //convolution matrix dimention 1 if you want a 3x3 convolution matrix it must be 3 #define QP //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< NP/2; i++){ for(j=Q/2; j< MQ/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< NP/2; i++){ for(j=Q/2; j< MQ/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进行自动矢量化时,它只适用于3×3卷积矩阵。 我已经看到汇编输出和自动矢量化只是对3×3内核进行了一些更改并合理地提高了性能(20倍快速注意:exception函数的标量版本比天真的乐趣慢)但是5×5卷积矩阵没有改进

更新:我在问题中加入了朴素的实现,并将图片大小更改为NxM,将矩阵转换为内核,将Cdim1xCdim2更改为PxQ,将seqConv函数更改为exception以进行说明。 问题不是改善exceptionfunction的实施。 问题是虽然所有元素都在内存的相同位置,但gcc使用启发式等等,为什么gcc无法改进这种不寻常的实现? 注意:问题不在于天真的实现。 gcc -O3通过~7加速改进了3×3,5×5内核的朴素实现。 它也可以通过~1.5加速来实现7×7和9×9。 为了改善卷积,我使用内在函数和加速比天真的实现超过40倍,比exception卷积快约2倍。 所以我的矢量化比我不常见的快80倍。 手调整优化不是问题。 自动矢量化器优化是问题,并且失败的原因。

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

平台:Linux mint,Skylake,gcc 6.2

提前致谢

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

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

来自维基百科 :

在第一步中,编译器会查找可能阻止矢量化的障碍。 矢量化的主要障碍是真实的数据依赖性短于矢量长度。 其他障碍包括函数调用和短迭代计数。

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

在我的机器上,我声明如下:

 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的卷积矩阵
  • 使用一些零填充输入,以便最后的未定义行为消失
  • 请注意,这实际上并没有增加你的性能。 事实上它工作得更慢!
  • 请注意,如果您按照以下顺序执行循环,则可以挤压几个周期:(ki)for(i)for(j)for(kj)。 这可能是由于寄存器压力较小,因为每行conv可以存储更长时间。 这可能也是我CPU上的一个小问题。
  • 在声明变量时,您可能还需要考虑使用__attribute__ ((aligned (8))) 。 在这种情况下,它没有改变任何东西,但在优化时你也想要考虑这一点。 当然,这只适用于GCC,你需要其他的MSVC黑客攻击。

自动矢量化器的主要障碍是非恒定循环变量。 在您的实现中,如果使用int Csize = P*Q; 它不会被矢量化。 所以为了帮助你自动矢量,你应该考虑这个。 这不是问题,因为你宣称Csize#define Csize 。 但请注意你的作品。 然后你的不寻常的实现是nave实现的循环转换,这是编译器中的优化方法。 看来你破坏了天真的实现。 你的发现说它受限制因为16所以我展开你的不寻常的function和自动矢量化器说它已被矢量化。

 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基于展开!