Авто-векторизация команды перемешивания - PullRequest
0 голосов
/ 02 февраля 2019

Я пытаюсь заставить компилятор сгенерировать инструкцию (v)pshufd (или эквивалентную) через авто-векторизацию.Это удивительно сложно.

Например, если предположить, что вектор имеет значения 4 uint32, преобразование: A|B|C|D => A|A|C|C должно быть достигнуто с помощью одной инструкции (соответствующей встроенной функции: _mm_shuffle_epi32()).

Пытаясь выразить то же преобразование, используя только обычные операции, я могу написать, например:

    for (i=0; i<4; i+=2)
        v32x4[i] = v32x4[i+1];

Компилятор, кажется, не может выполнить хорошее преобразование, генерируя вместо этого смесь скалярного и векторного кодаболее десятка инструкций.Развертывание вручную приводит к еще худшему результату.

Иногда мешают небольшие детали, мешающие компилятору корректно переводить.Например, nb элементов в массиве должно иметь четкую степень 2, указатели на таблицу должны быть гарантированно не псевдонимами, выравнивание должно быть выражено явно и т. Д. В этом случае я не нашел подобной причины, иЯ все еще придерживаюсь ручных встроенных функций для создания разумной сборки.

Есть ли способ сгенерировать инструкцию (v)pshufd, используя только нормальный код и полагаясь на авто-векторизатор компилятора?

1 Ответ

0 голосов
/ 02 февраля 2019

(Обновление: новый ответ с 2019-02-07.)

Можно заставить компилятор генерировать инструкцию (v)pshufd, даже без расширений вектора gcc, которые я использовал в предыдущийответ на этот вопрос .Следующие примеры дают представление о возможностях.Эти примеры скомпилированы с gcc 8.2 и clang 7.

Пример 1

#include<stdint.h>
/*                                       vectorizes     */
/*   gcc -m64 -O3  -march=nehalem        Yes            */
/*   gcc -m64 -O3  -march=skylake        Yes            */
/*   clang -m64 -O3  -march=nehalem      No             */
/*   clang -m64 -O3  -march=skylake      No             */
void shuff1(int32_t* restrict a, int32_t* restrict b, int32_t n){
    /* this line is optional */  a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
    for (int32_t i = 0; i < n; i=i+4) {
        b[i+0] = a[i+0];
        b[i+1] = a[i+0];
        b[i+2] = a[i+2];
        b[i+3] = a[i+2];
    }
}


/*                                       vectorizes     */
/*   gcc -m64 -O3  -march=nehalem        Yes            */
/*   gcc -m64 -O3  -march=skylake        Yes            */
/*   clang -m64 -O3  -march=nehalem      Yes            */
/*   clang -m64 -O3  -march=skylake      Yes            */
void shuff2(int32_t* restrict a, int32_t* restrict b, int32_t n){
    /* this line is optional */  a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
    for (int32_t i = 0; i < n; i=i+4) {
        b[i+0] = a[i+1];
        b[i+1] = a[i+2];
        b[i+2] = a[i+3];
        b[i+3] = a[i+0];
    }
}

Удивительно, но кланг векторизует только перестановки в математическом смысле, а не общие перемешивания.При gcc -m64 -O3 -march=nehalem основной цикл shuff1 становится:

.L3:
  add edx, 1
  pshufd xmm0, XMMWORD PTR [rdi+rax], 160
  movaps XMMWORD PTR [rsi+rax], xmm0
  add rax, 16
  cmp edx, ecx
  jb .L3

Пример 2

/*                                       vectorizes     */
/*   gcc -m64 -O3  -march=nehalem        No             */
/*   gcc -m64 -O3  -march=skylake        No             */
/*   clang -m64 -O3  -march=nehalem      No             */
/*   clang -m64 -O3  -march=skylake      No             */
void shuff3(int32_t* restrict a, int32_t* restrict b){
    /* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
    b[0] = a[0];
    b[1] = a[0];
    b[2] = a[2];
    b[3] = a[2];
}


/*                                       vectorizes     */
/*   gcc -m64 -O3  -march=nehalem        Yes            */
/*   gcc -m64 -O3  -march=skylake        Yes            */
/*   clang -m64 -O3  -march=nehalem      Yes            */
/*   clang -m64 -O3  -march=skylake      Yes            */
void shuff4(int32_t* restrict a, int32_t* restrict b){
    /* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
    b[0] = a[1];
    b[1] = a[2];
    b[2] = a[3];
    b[3] = a[0];
}

Сборка с gcc -m64 -O3 -march=skylake:

shuff3:
  mov eax, DWORD PTR [rdi]
  mov DWORD PTR [rsi], eax
  mov DWORD PTR [rsi+4], eax
  mov eax, DWORD PTR [rdi+8]
  mov DWORD PTR [rsi+8], eax
  mov DWORD PTR [rsi+12], eax
  ret
shuff4:
  vpshufd xmm0, XMMWORD PTR [rdi], 57
  vmovaps XMMWORD PTR [rsi], xmm0
  ret

И снова результаты перестановки (0,3,2,1) существенно отличаютсяиз (2,2,0,0) случайного случая.

Пример 3

/*                                       vectorizes     */
/*   gcc -m64 -O3  -march=nehalem        Yes            */
/*   gcc -m64 -O3  -march=skylake        Yes            */
/*   clang -m64 -O3  -march=nehalem      No             */
/*   clang -m64 -O3  -march=skylake      No             */
void shuff5(int32_t* restrict a, int32_t* restrict b, int32_t n){
    /* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 32); b = (int32_t*)__builtin_assume_aligned(b, 32);
    for (int32_t i = 0; i < n; i=i+8) {
        b[i+0] = a[i+2];
        b[i+1] = a[i+7];
        b[i+2] = a[i+7];
        b[i+3] = a[i+7];
        b[i+4] = a[i+0];
        b[i+5] = a[i+1];
        b[i+6] = a[i+5];
        b[i+7] = a[i+4];
    }
}


/*                                       vectorizes     */
/*   gcc -m64 -O3  -march=nehalem        Yes            */
/*   gcc -m64 -O3  -march=skylake        Yes            */
/*   clang -m64 -O3  -march=nehalem      No             */
/*   clang -m64 -O3  -march=skylake      No             */
void shuff6(int32_t* restrict a, int32_t* restrict b, int32_t n){
    /* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 32); b = (int32_t*)__builtin_assume_aligned(b, 32);
    for (int32_t i = 0; i < n; i=i+8) {
        b[i+0] = a[i+0];
        b[i+1] = a[i+0];
        b[i+2] = a[i+2];
        b[i+3] = a[i+2];
        b[i+4] = a[i+4];
        b[i+5] = a[i+4];
        b[i+6] = a[i+6];
        b[i+7] = a[i+6];
    }
}

С gcc -m64 -O3 -march=skylake в основном цикле shuff5 содержит инструкцию по пересечению полосы vpermd shuffle, что, я думаю, впечатляет.Функция shuff6 ведет к инструкции без пересечения vpshufd ymm0, mem, отлично.

Пример 4

Сборка shuff5 становится довольно грязной, если мы заменим b[i+5] = a[i+1]; на b[i+5] = 0;.Тем не менее цикл был векторизован.Смотрите также Godbolt link для всех примеров, обсуждаемых в этом ответе.

Если массивы a и b выровнены по 16 (или 32) байтам, то мы можем использовать a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16); (или 32 вместо 16).Иногда это немного улучшает генерацию ассемблерного кода.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...