Как избежать ошибки AVX2, когда размерность матрицы не кратна 4?

Я сделал программу умножения матрицы на вектор, используя AVX2, FMA на C. Я скомпилировал, используя GCC ver7 с -mfma, -mavx.

Однако я получил сообщение об ошибке "неверная контрольная сумма для освобожденного объекта - объект, вероятно, был изменен после освобождения".

Я думаю, что ошибка будет генерироваться, если размер матрицы не кратен 4.

Я знаю, что AVX2 использует регистр ymm, который может использовать 4 числа с плавающей запятой двойной точности. Поэтому я могу использовать AVX2 без ошибок, если матрица кратна 4.

Но, вот мой вопрос. Как я могу эффективно использовать AVX2, если матрица не кратна 4???

Вот мой код.

#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;
}

person Mic    schedule 22.07.2018    source источник


Ответы (1)


Вы загружаете и сохраняете векторы из 4 double, но ваше условие цикла проверяет только то, что первый элемент вектора находится внутри границ, поэтому вы можете записывать внешние объекты размером до 3x8 = 24 байта, когда m не является кратное 4.

Вам нужно что-то вроде i < (m-3) в основном цикле и стратегия очистки для обработки последнего частичного вектора данных. Векторизация с помощью SIMD очень похожа на развертывание: вы должны проверить, можно ли выполнять несколько будущих элементов в условии цикла.

Скалярный цикл очистки работает хорошо, но мы можем добиться большего. Например, сделайте как можно больше 128-битных векторов после последнего полного 256-битного вектора (т. е. до 1), прежде чем переходить к скалярному.

Во многих случаях (например, место назначения только для записи) невыровненная векторная загрузка, которая заканчивается в конце ваших массивов, очень хороша (когда 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<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. Иногда компиляторы пытаются переусердствовать с кодом очистки (например, пытаются автоматически векторизовать скалярные циклы очистки).

Другие стратегии могут включать в себя выполнение последних до 4 элементов с хранилищем по маске AVX: вам нужна одна и та же маска для конца каждой строки матрицы, поэтому может быть хорошо сгенерировать ее один раз, а затем использовать в конце каждой строки. См. векторизацию с невыровненными буферами: использование VMASKMOVPS: генерация маска от рассогласования кол? Или вообще не использовать этот insn. (Чтобы упростить ветвление, вы должны настроить его таким образом, чтобы ваш основной цикл переходил только к i < (m-4), а затем вы всегда запускали очистку. В случае m%4 == 0 маска — это все единицы, поэтому вы выполняете окончательный полный вектор.) Если вы не можете безопасно читать дальше конца матрицы, вам, вероятно, нужна маскированная загрузка, а также маскированное сохранение.


Вы также можете рассмотреть возможность выравнивания строк для повышения эффективности или шага строки, который отличается от логической длины строк. (т.е. заполнять строки до 32-байтовых границ). Оставляя отступы в конце строк, вы упрощаете очистку, потому что вы всегда можете сделать целые векторы, которые записывают отступы.


Частный случай m==2: вместо того, чтобы транслировать один элемент из b[], вы хотите транслировать 2 элемента в две 128-битные дорожки __m256d, чтобы один 256-битный FMA мог обрабатывать 2 строки одновременно.

person Peter Cordes    schedule 22.07.2018