当矩阵维数不是4的倍数时,如何避免AVX2的错误?

我使用AVX2,C中的FMA制作了矩阵向量乘法程序。我使用GCC ver7编译了-mfma,-mavx。

但是,我收到错误“释放对象的校验和不正确 – 对象可能在被释放后被修改。”

我认为如果矩阵维度不是4的倍数,则会产生错误。

我知道AVX2使用ymm寄存器,可以使用4个双精度浮点数。 因此,如果矩阵是4的倍数,我可以毫无错误地使用AVX2。

但是,这是我的问题。 如果矩阵不是4的倍数,我怎样才能有效地使用AVX2 ???

这是我的代码。

#include "stdio.h" #include "math.h" #include "stdlib.h" #include "time.h" #include "x86intrin.h" void mv(double *a,double *b,double *c, int m, int n, int l) { __m256d va,vb,vc; int k; int i; for (k = 0; k < l; k++) { vb = _mm256_broadcast_sd(&b[k]); for (i = 0; i < m; i+=4) { va = _mm256_loadu_pd(&a[m*k+i]); vc = _mm256_loadu_pd(&c[i]); vc = _mm256_fmadd_pd(vc, va, vb); _mm256_storeu_pd( &c[i], vc ); } } } int main(int argc, char* argv[]) { // set variables int m; double* a; double* b; double* c; int i; int temp=0; struct timespec startTime, endTime; m=9; // main program // set vector or matrix a=(double *)malloc(sizeof(double) * m*m); b=(double *)malloc(sizeof(double) * m*1); c=(double *)malloc(sizeof(double) * m*1); for (i=0;i<m;i++) { a[i]=1; b[i]=1; c[i]=0.0; } for (i=m;i<m*m;i++) { a[i]=1; } // check start time clock_gettime(CLOCK_REALTIME, &startTime); mv(a, b, c, m, 1, m); // check end time clock_gettime(CLOCK_REALTIME, &endTime); free(a); free(b); free(c); return 0; } 

加载和存储4 double向量,但是你的循环条件只检查第一个向量元素是入界的,所以当m不是4的倍数时,你可以写出外部对象最多3×8 = 24个字节。

你需要在主循环中使用类似i < (m-3)东西,以及用于处理最后一个部分数据向量的清理策略。 使用SIMD进行矢量化非常类似于展开:您必须检查在循环条件下执行多个未来元素是否可行。

标量清理循环运行良好,但我们可以做得更好。 例如,在去标量之前,在最后一个完整的256位向量(即最多1)之后执行尽可能多的128位向量。

在许多情况下(例如,只写目标),在数组末尾结束的未对齐矢量加载非常好(当m>=4 )。 如果m%4 != 0 ,它可以与你的主循环重叠,但这很好,因为你的输出数组不会与你的输入重叠,因此作为单个清理的一部分重做元素比分支避免它更便宜。

但这在这里不起作用,因为你的逻辑是c[i+0..3] += ... ,所以重做一个元素会使它错误。

 // cleanup using a 128-bit FMA, then scalar if there's an odd element. // untested void mv(double *a,double *b,double *c, int m, int n, int l) { /* the loop below should actually work for m=1..3, but a separate strategy might be good. if (m < 4) { // maybe check m >= 2 and use __m128 vectors? // or vectorize differently? } */ for (int k = 0; k < l; k++) { __m256 vb = _mm256_broadcast_sd(&b[k]); int i; for (i = 0; i < (m-3); i+=4) { __m256d va = _mm256_loadu_pd(&a[m*k+i]); __m256d vc = _mm256_loadu_pd(&c[i]); vc = _mm256_fmadd_pd(vc, va, vb); _mm256_storeu_pd( &c[i], vc ); } if (i<(m-1)) { __m128d lasta = _mm_loadu_pd(&a[m*k+i]); __m128d lastc = _mm_loadu_pd(&c[i]); lastc = _mm_fmadd_pd(lastc, va, _mm256_castpd256_pd128(vb)); _mm_storeu_pd( &c[i], lastc ); // i+=2; // last element only checks m odd/even, doesn't use i } // if (i 

我还没有看到gcc / clang -O3如何编译。 有时编译器会尝试使用清理代码过于聪明(例如,尝试自动向量化标量清理循环)。

其他策略可能包括使用AVX蒙版存储执行最后的4个元素:在每个矩阵行的末尾需要相同的掩码,因此生成它一次然后在每行的末尾使用它可能是好的。 请参阅使用未对齐缓冲区进行矢量化:使用VMASKMOVPS:根据未对齐计数生成掩码? 或者根本不使用那个insn 。 (为了简化分支,你要设置它,这样你的主循环才会进入i < (m-4) ,然后你总是运行清理。在m%4 == 0情况下,掩码是全部的,所以你做最后的完整向量。)如果你不能安全地读取矩阵的结尾,你可能需要一个蒙版加载以及蒙版存储。


您还可以查看对齐行以提高效率,或者将行跨步与行的逻辑长度分开。 (即填充行到32字节边界)。 在行的末尾保留填充简化了清理,因为您始终可以执行写入填充的整个向量。


特殊情况m==2 :您不想从b[]广播一个元素,而是想将2个元素广播到__m256d两个128位通道中,因此一个256位FMA可以一次执行2行。