1

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

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

我认为如果矩阵维度不是 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

1 回答 1

2

您加载和存储 4 的向量double,但您的循环条件仅检查第一个m向量元素是否在界内,因此当不是 4 的倍数时,您最多可以将外部对象写入 3x8 = 24 个字节。

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

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

在许多情况下(例如只写目标),在数组末尾结束的未对齐向量加载非常好(当m>=4)。它可以与您的主循环重叠 if 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<m)
        if (m&1) {
            // odd number of elements, do the last non-vector one
            c[m-1] += a[m*k + m-1] * _mm256_cvtsd_f64(vb);
        }

    }
}

我还没有仔细研究 gcc/clang -O3 是如何编译它的。有时编译器会尝试使用清理代码变得过于聪明(例如,尝试自动矢量化标量清理循环)。

其他策略可能包括使用 AVX 掩码存储执行最后最多 4 个元素:您需要在每个矩阵行的末尾使用相同的掩码,因此生成一次然后在每行的末尾使用它可能会很好。请参阅使用未对齐缓冲区进行矢量化:使用 VMASKMOVPS:根据未对齐计数生成掩码?或者根本不使用那个insn。(为了简化分支,您将其设置为使您的主循环仅转到i < (m-4),然后您始终运行清理。在这种m%4 == 0情况下,掩码为全一,因此您执行最终的全向量。)如果您不能安全地读取矩阵的末尾,您可能需要屏蔽加载和屏蔽存储。


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


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

于 2018-07-22T08:34:14.130 回答