Использование встроенной сборки для ускорения умножения матриц

Я пытался ускорить умножение матрицы на матрицу C ‹- C + alpha * A * B с помощью блокировки регистров, векторизации SSE2 и блокировки кеша L1 (обратите внимание, что я специально выбрал параметр транспонирования op (A) = A и op ( Б) = Б). После некоторых усилий мой написанный код все еще примерно на 50% медленнее, чем GotoBLAS в однопоточном режиме.

Ниже приведен мой код для умножения квадратной матрицы-матрицы "ядра" в кэше L1, называемого "DGEBB" (общая блочно-блочная операция) в работе Goto, который умножает две квадратные матрицы NB * NB (NB ограничено, чтобы быть кратным 4). Я изучил его сборку в GCC 4.8 и понял, что компилятор плохо справляется с планированием развернутого внутреннего цикла: kk-loop. Я надеюсь, что компилятор оптимизирует распределение регистров для достижения повторного использования регистров и планирует вычисление с чередованием умножения, сложения и операции с памятью для конвейерной обработки; однако компилятору это не удалось. По этой причине я хотел бы заменить самый внутренний цикл некоторой встроенной сборкой.

Я совершенно новичок в сборке x86. Хотя я часами читал расширенный asm GCC, я все еще не уверен, как это сделать правильно. Я прикрепил дурацкую версию, которую мог бы написать в моих силах, но зная, что это неверно. Эта версия является изменением исходных выходных данных сборки компилятора для цикла kk. Поскольку я знаю, как выделить регистр с помощью "movl", "movapd" и т. Д., Я перестроил вычисления в том порядке, который мне нравится. Но пока не работает. 1) Мне кажется, что регистры% eax,% ebx,% ecx используются как внутри, так и вне сборки, что неприятно. 2) Кроме того, способ передачи операндов ввода и вывода не работает. 3) Наконец, мне действительно нужна версия, в которой весь цикл kk может быть встроен. Спасибо, если кто-то может мне помочь!

Код C для DGEBB (называется DGEBB_SSE2_x86, поскольку мой ноутбук - 32-разрядная машина x86 с поддержкой SSE2 - SSE4.1):

#include <stdint.h>  /* type define of "uintptr_t" */
#include <emmintrin.h>  /* double precision computation support since SSE2 */
#include <R.h>  /* use R's error handling error() */

void DGEBB_SSE2_x86 (int *NB, double *ALPHA, double *A, double *B, double *C) {
/* check "nb", must be a multiple of 4 */
int TWO=2, FOUR=4, nb=*NB; if (nb%FOUR) error("error in DGEBB_SSE2_x86: nb is not a multiple of 4!\n");
/* check memory alignment of A, B, C, 16 Byte alignment is mandatory (as XMM registers are 128-bit in length) */
uintptr_t sixteen_bytes=0xF;
if ((uintptr_t)A & sixteen_bytes) error("error in DGEBB_SSE2_x86: A is not 16 Bytes aligned in memory!");
if ((uintptr_t)B & sixteen_bytes) error("error in DGEBB_SSE2_x86: B is not 16 Bytes aligned in memory!");
if ((uintptr_t)C & sixteen_bytes) error("error in DGEBB_SSE2_x86: C is not 16 Bytes aligned in memory!");
/* define vector variables */
__m128d C1_vec_reg=_mm_setzero_pd(), C2_vec_reg=C1_vec_reg, C3_vec_reg=C1_vec_reg, C4_vec_reg=C1_vec_reg,A1_vec_reg, A2_vec_reg, B_vec_reg, U_vec_reg;
/* define scalar variables */
int jj, kk, ii, nb2=nb+nb, nb_half=nb/TWO;
double *B1_copy, *B1, *C1, *a, *b, *c, *c0;
/* start triple loop nest */
C1=C;B1=B;  /* initial column tile of C and B */
jj=nb_half;
while (jj--) {
  c=C1;B1_copy=B1;C1+=nb2;B1+=nb2;b=B1_copy;
  for (ii=0; ii<nb; ii+=FOUR) {
    a=A+ii;b=B1_copy;
    kk=nb_half;
    while (kk--) {
    /* [kernel] amortize pointer arithmetic! */
    A1_vec_reg=_mm_load_pd(a);  /* [fetch] */
    B_vec_reg=_mm_load1_pd(b);  /* [fetch] */
    U_vec_reg=_mm_mul_pd(A1_vec_reg,B_vec_reg);C1_vec_reg=_mm_add_pd(C1_vec_reg,U_vec_reg);  /* [daxpy] */
    A2_vec_reg=_mm_load_pd(a+TWO);a+=nb;  /* [fetch] */
    U_vec_reg=_mm_mul_pd(A2_vec_reg,B_vec_reg);C2_vec_reg=_mm_add_pd(C2_vec_reg,U_vec_reg);  /* [daxpy] */
    B_vec_reg=_mm_load1_pd(b+nb);b++;  /* [fetch] */
    U_vec_reg=_mm_mul_pd(A1_vec_reg,B_vec_reg);C3_vec_reg=_mm_add_pd(C3_vec_reg,U_vec_reg);  /* [daxpy] */
    A1_vec_reg=_mm_load_pd(a);  /* [fetch] */
    U_vec_reg=_mm_mul_pd(A2_vec_reg,B_vec_reg);C4_vec_reg=_mm_add_pd(C4_vec_reg,U_vec_reg);  /* [daxpy]*/
    B_vec_reg=_mm_load1_pd(b);  /* [fetch] */
    U_vec_reg=_mm_mul_pd(A1_vec_reg,B_vec_reg);C1_vec_reg=_mm_add_pd(C1_vec_reg,U_vec_reg);  /* [daxpy] */
    A2_vec_reg=_mm_load_pd(a+TWO);a+=nb;  /* [fetch] */
    U_vec_reg=_mm_mul_pd(A2_vec_reg,B_vec_reg);C2_vec_reg=_mm_add_pd(C2_vec_reg,U_vec_reg);  /* [daxpy] */
    B_vec_reg=_mm_load1_pd(b+nb);b++;  /* [fetch] */
    U_vec_reg=_mm_mul_pd(A1_vec_reg,B_vec_reg);C3_vec_reg=_mm_add_pd(C3_vec_reg,U_vec_reg);  /* [daxpy] */
    U_vec_reg=_mm_mul_pd(A2_vec_reg,B_vec_reg);C4_vec_reg=_mm_add_pd(C4_vec_reg,U_vec_reg);  /* [daxpy] */
    }  /* [end of kk-loop] */
  /* [write-back] amortize pointer arithmetic! */
  A2_vec_reg=_mm_load1_pd(ALPHA);
  U_vec_reg=_mm_load_pd(c);c0=c+nb;C1_vec_reg=_mm_mul_pd(C1_vec_reg,A2_vec_reg);  /* [fetch] */
  A1_vec_reg=U_vec_reg;C1_vec_reg=_mm_add_pd(C1_vec_reg,A1_vec_reg);U_vec_reg=_mm_load_pd(c0);  /* [fetch] */
  C3_vec_reg=_mm_mul_pd(C3_vec_reg,A2_vec_reg);_mm_store_pd(c,C1_vec_reg);c+=TWO;  /* [store] */
  A1_vec_reg=U_vec_reg;C3_vec_reg=_mm_add_pd(C3_vec_reg,A1_vec_reg);U_vec_reg=_mm_load_pd(c);  /* [fetch] */
  C2_vec_reg=_mm_mul_pd(C2_vec_reg,A2_vec_reg);_mm_store_pd(c0,C3_vec_reg);c0+=TWO;  /* [store] */
  A1_vec_reg=U_vec_reg;C2_vec_reg=_mm_add_pd(C2_vec_reg,A1_vec_reg);U_vec_reg=_mm_load_pd(c0);  /* [fetch] */
  C4_vec_reg=_mm_mul_pd(C4_vec_reg,A2_vec_reg);_mm_store_pd(c,C2_vec_reg);c+=TWO;  /* [store] */
  C4_vec_reg=_mm_add_pd(C4_vec_reg,U_vec_reg);_mm_store_pd(c0,C4_vec_reg);  /* [store] */
  C1_vec_reg=_mm_setzero_pd();C3_vec_reg=C1_vec_reg;C2_vec_reg=C1_vec_reg;C4_vec_reg=C1_vec_reg;
  }  /* [end of ii-loop] */
}  /* [end of jj-loop] */
}

Моя дурацкая версия встроенной сборки для kk-цикла здесь:

      while (kk--) {
    asm("movapd %0, %%xmm3\n\t"     /* C1_vec_reg -> xmm3 */
        "movapd %1, %%xmm1\n\t"     /* C2_vec_reg -> xmm1 */
        "movapd %2, %%xmm2\n\t"     /* C3_vec_reg -> xmm2 */
        "movapd %3, %%xmm0\n\t"     /* C4_vec_reg -> xmm0 */
        "movl %4, %%eax\n\t"    /* pointer a -> %eax */
        "movl %5, %%edx\n\t"    /* pointer b -> %edx */
        "movl %6, %%ecx\n\t"    /* block size nb -> %ecx */
        "movapd (%%eax), %%xmm5\n\t"   /* A1_vec_reg -> xmm5 */
    "movsd (%%edx), %%xmm4\n\t"        /* B_vec_reg -> xmm4 */
    "unpcklpd %%xmm4, %%xmm4\n\t"
        "movapd %%xmm5, %%xmm6\n\t"        /* xmm5 -> xmm6 */
        "mulpd %%xmm4, %%xmm6\n\t"        /* xmm6 *= xmm4 */
    "addpd %%xmm6, %%xmm3\n\t"        /* xmm3 += xmm6 */
        "movapd 16(%%eax), %%xmm7\n\t"        /* A2_vec_reg -> xmm7 */
        "movapd %%xmm7, %%xmm6\n\t"        /* xmm7 -> xmm6 */
        "mulpd %%xmm4, %%xmm6\n\t"        /* xmm6 *= xmm4 */
    "addpd %%xmm6, %%xmm1\n\t"        /* xmm1 += xmm6 */
    "movsd (%%edx,%%ecx), %%xmm4\n\t"        /* B_vec_reg -> xmm4 */
    "addl $8, %%edx\n\t"          /* b++ */
    "movsd (%%edx), %%xmm4\n\t"       /* B_vec_reg -> xmm4 */
    "unpcklpd %%xmm4, %%xmm4\n\t"
        "movapd %%xmm5, %%xmm6\n\t"        /* xmm5 -> xmm6 */
        "mulpd %%xmm4, %%xmm6\n\t"        /* xmm6 *= xmm4 */
    "addpd %%xmm6, %%xmm2\n\t"        /* xmm2 += xmm6 */
    "addl %%ecx, %%eax\n\t"          /* a+=nb */
    "movapd (%%eax), %%xmm5\n\t"        /* A1_vec_reg -> xmm5 */
        "movapd %%xmm7, %%xmm6\n\t"        /* xmm7 -> xmm6 */
        "mulpd %%xmm4, %%xmm6\n\t"        /* xmm6 *= xmm4 */
    "addpd %%xmm6, %%xmm0\n\t"      /* xmm0 += xmm6 */
    "movsd (%%edx), %%xmm4\n\t"        /* B_vec_reg -> xmm4 */
    "unpcklpd %%xmm4, %%xmm4\n\t"
        "movapd %%xmm5, %%xmm6\n\t"        /* xmm5 -> xmm6 */
        "mulpd %%xmm4, %%xmm6\n\t"        /* xmm6 *= xmm4 */
        "addpd %%xmm6, %%xmm3\n\t"        /* xmm3 += xmm6 */
        "movapd 16(%%eax), %%xmm7\n\t"        /* A2_vec_reg -> xmm7 */
        "movapd %%xmm7, %%xmm6\n\t"        /* xmm7 -> xmm6 */
        "mulpd %%xmm4, %%xmm6\n\t"        /* xmm6 *= xmm4 */
    "addpd %%xmm6, %%xmm1\n\t"        /* xmm1 += xmm6 */
    "movsd (%%edx,%%ecx), %%xmm4\n\t"        /* B_vec_reg -> xmm4 */
    "addl $8, %%edx\n\t"          /* b++ */
    "movsd (%%edx), %%xmm4\n\t"       /* B_vec_reg -> xmm4 */
    "unpcklpd %%xmm4, %%xmm4\n\t"
        "movapd %%xmm5, %%xmm6\n\t"        /* xmm5 -> xmm6 */
        "mulpd %%xmm4, %%xmm6\n\t"        /* xmm6 *= xmm4 */
    "addpd %%xmm6, %%xmm2\n\t"        /* xmm2 += xmm6 */
        "movapd %%xmm7, %%xmm6\n\t"        /* xmm7 -> xmm6 */
        "mulpd %%xmm4, %%xmm6\n\t"        /* xmm6 *= xmm4 */
    "addpd %%xmm6, %%xmm0\n\t"      /* xmm0 += xmm6 */
    "addl %%ecx, %%eax"
        : "+x"(C1_vec_reg), "+x"(C2_vec_reg), "+x"(C3_vec_reg), "+x"(C4_vec_reg), "+m"(a), "+m"(b)
        : "x"(C1_vec_reg), "x"(C2_vec_reg), "x"(C3_vec_reg), "x"(C4_vec_reg), "4"(a), "5"(b), "rm"(nb)); 
}

Вот некоторые пояснения к коду:

Unrolling out loops to expose a micro "dger" kernel for register resue:
 (c11 c12) += (a1) * (b1 b2)
 (c21 c22)    (a2)
 (c31 c32)    (a3)
 (c41 c42)    (a4)
This can be implemented as 4 vectorized "daxpy":
 (c11) += (a1) * (b1)  ,  (c31) += (a3) * (b1)  ,  (c12) += (a1) * (b2)  ,  (c32) += (a3) * (b2)  .
 (c21)    (a2)   (b1)     (c41)    (a4)   (b1)     (c22)    (a2)   (b2)     (c42)    (a4)   (b2)
4 micor C-vectors are held constantly in XMM registers named C1_vec_reg, C2_vec_reg, C3_vec_reg, C4_vec_reg.
2 micro A-vectors are loaded into XMM registers named A1_vec_reg, A2_vec_reg.
2 micro B-vectors can reuse a single XMM register named B_vec_reg.
1 additional XMM register, U_vec_reg, will store temporary values.
The above scheduling exploits all 8 XMM registers on x84 architectures with SIMD unit, and each XMM is used twice after loaded.

PS: Я пользователь R из группы статистики. Заголовочный файл позволяет использовать функцию error () R для обработки ошибок. Это просто завершит программу C, а не весь процесс R. Если вы не используете R, удалите эту строку и соответствующие строки в коде.


person Zheyuan Li    schedule 02.02.2016    source источник
comment
У Роберта ван де Гейна есть хороший учебник по GEMM. Я постараюсь найти для вас ссылку позже. Проект BLIS и документы по нему также могут помочь.   -  person Jeff Hammond    schedule 02.02.2016
comment
Это может быть невежественный вопрос с моей стороны, но вы сбросили сборку, сгенерированную вашим кодом C, с различными уровнями оптимизации, чтобы определить, как компилятор обрабатывает ее с самого начала?   -  person David C. Rankin    schedule 02.02.2016
comment
Вы, наверное, уже знаете это, но удивительно, как часто люди не знают об этом - вы компилируете с -O3, верно?   -  person Paul R    schedule 02.02.2016
comment
Вы ошиблись: ваш код C трудно читать, потому что вы упаковываете свои операторы в прямоугольную матрицу с очень небольшим количеством пробелов, как если бы они замедляли время выполнения. Смешивание сборки с этим только еще больше запутает реализацию, скроет ошибки в более тонких местах и ​​предотвратит дальнейшую оптимизацию компилятора для будущих целей с большим количеством регистров, более широкими векторами ... Оцените различные компиляторы с различными настройками оптимизации, проверьте расширенные библиотеки, и, конечно же, изучите свои алгоритмы на предмет потенциальных улучшений.   -  person chqrlie    schedule 02.02.2016
comment
@AlphaBetaGamma: с очень небольшим количеством пробелов, как если бы они замедляли время выполнения. Это было немного сарказмом, интересно, откуда у тебя свой особый стиль.   -  person chqrlie    schedule 03.02.2016


Ответы (1)


Это старая проблема, возникшая на ранней стадии разработки моей процедуры факторизации HPC Cholesky. Код на C устарел, а сборка наивно некорректна. Более поздние сообщения следуют за этой веткой.

(встроенная сборка на C) Сообщения ассемблера: Ошибка: неизвестный псевдооператор: дает правильную реализацию встроенной сборки.

Как попросить GCC полностью развернуть этот цикл (т.е. очистить этот цикл)? дает лучший код C.

При написании встроенной сборки GCC необходимо учитывать возможные изменения флага состояния. (встроенная сборка на C) Забавная ошибка сегментации памяти - это урок для меня.

Векторизация - ключ к высокопроизводительным вычислениям. Инструкция SSE MOVSD (расширенная: скалярные и векторные операции с плавающей запятой на x86, x86-64) содержит некоторые обсуждения Intel SSE2 / 3, а инструкция FMA _mm256_fmadd_pd (): 132, 231 и 213? содержит некоторую информацию об инструкции Intel AVX FMA.

Конечно, все это относится только к вычислительным ядрам. Есть много другой работы, связанной с тем, как все обернуть для финальной высокопроизводительной процедуры факторизации Холецкого. Производительность первого выпуска моей подпрограммы находится в Почему мой ЦП не может поддерживать пиковую производительность в HPC < / а>.

В настоящее время я обновляю подпрограмму ядра для еще большей производительности. Возможно, в этой теме появятся и другие сообщения. Благодаря сообществу переполнения стека, особенно Z-бозон, Питер Кордес и номинальное животное за ответы на различные мои вопросы. Я многому научился и чувствую себя очень счастливым в этом процессе. [Конечно, в то же время я научился быть лучшим членом SO.]

person Zheyuan Li    schedule 04.04.2016