Самый быстрый способ заполнить вектор (SSE2) определенным значением. Шаблоны дружественные

У меня есть этот шаблон класса:

template<size_t D>
struct A{
    double v_sse __attribute__ ((vector_size (8*D)));
    A(double val){
        //what here?
    }
};

Какой лучший способ заполнить v_sse поле с копиями val? Поскольку я использую векторы, я могу использовать встроенные функции gcc SSE2.

1 ответ

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

Я получил тот же результат, что и @hirschhornsalz: массивный, неэффективный код при его создании с векторами, превышающими размеры векторов, поддерживаемые HW. например, строительство A<8> без AVX512 выдает лодку с 64 бит mov а также vmovsd инструкции. Он выполняет одну трансляцию на локальный стек, а затем считывает все эти значения отдельно и записывает их в буфер struct-return вызывающей стороны.

Для x86 мы можем заставить gcc выдавать оптимальные широковещательные сообщения для функции, которая принимает double arg (в xmm0) и возвращает вектор (в x/y/zmm0) в соответствии со стандартными соглашениями о вызовах:

  • SSE2: unpckpd xmm0, xmm0
  • SSE3: movddup xmm0, xmm0
  • AVX: vmovddup xmm0, xmm0 / vinsertf128 ymm0, ymm0, xmm0, 1
    (AVX1 включает только vbroadcastsd ymm, m64 форма, которая, вероятно, будет использоваться, если встроена при вызове данных в памяти)
  • AVX2: vbroadcastsd ymm0, xmm0
  • AVX512: vbroadcastsd zmm0, xmm0, (Обратите внимание, что AVX512 может транслировать из mem на лету:
    VADDPD zmm1 {k1}{z}, zmm2, zmm3/m512/m64bcst{er}
    {k1}{z} означает, что он может использовать регистр маски как результат слияния или нулевую маску в результате.
    m64bcst означает 64-битный адрес памяти для трансляции.
    {er} означает, что режим округления MXCSR может быть переопределен для этой одной инструкции.
    IDK, если gcc будет использовать этот режим широковещательной адресации для сложения широковещательных загрузок в операнды памяти.

Тем не менее, gcc также понимает тасования и имеет __builtin_shuffle для произвольных размеров вектора. С постоянной маской времени компиляции, состоящей из всех нулей, случайное перемешивание становится широковещательной передачей, что gcc делает, используя лучшую инструкцию для работы.

typedef int64_t v4di __attribute__ ((vector_size (32)));
typedef double  v4df __attribute__ ((vector_size (32)));
v4df vecinit4(double v) {
    v4df v_sse;
    typeof (v_sse) v_low = {v};
    v4di shufmask = {0};
    v_sse = __builtin_shuffle (v_low, shufmask );
    return v_sse;
}

В шаблонных функциях gcc 4.9.2, похоже, имеет проблему с распознаванием того, что оба вектора имеют одинаковую ширину и число элементов, и что маска является вектором типа int. Он ошибается даже без создания экземпляра шаблона, поэтому, возможно, именно поэтому у него есть проблема с типами. Все работает отлично, если я копирую класс и удаляю его из шаблона до определенного размера вектора.

template<int D> struct A{
    typedef double  dvec __attribute__ ((vector_size (8*D)));
    typedef int64_t ivec __attribute__ ((vector_size (8*D)));
    dvec v_sse;  // typeof(v_sse) is buggy without this typedef, in a template class
    A(double v) {
#ifdef SHUFFLE_BROADCAST  // broken on gcc 4.9.2
    typeof(v_sse)  v_low = {v};
    //int64_t __attribute__ ((vector_size (8*D))) shufmask = {0};
    ivec shufmask = {0, 0};
    v_sse = __builtin_shuffle (v_low, shufmask);  // no idea why this doesn't compile
#else
    typeof (v_sse) zero = {0, 0};
    v_sse = zero + v;  // doesn't optimize away without -ffast-math
#endif
    }
};

/*  doesn't work:
double vec2val  __attribute__ ((vector_size (16))) = {v, v};
double vec4val  __attribute__ ((vector_size (32))) = {v, v, v, v};
v_sse = __builtin_choose_expr (D == 2, vec2val, vec4val);
*/

Мне удалось получить GCC к внутренней ошибки компилятора при компиляции с -O0, векторы + шаблоны, кажется, нуждаются в некоторой работе. (По крайней мере, это было сделано в gcc 4.9.2, который в настоящее время поставляется Ubuntu. Возможно, апстрим улучшился.)

Первая идея, которая у меня возникла, и которую я оставил как запасной вариант, потому что shuffle не компилируется, заключается в том, что gcc неявно передает данные, когда вы используете оператор с вектором и скаляром. Так, например, добавление скаляра к вектору всех нулей поможет.

Проблема в том, что фактическое добавление не будет оптимизировано, если вы не используете -ffast-math, -funsafe-math-optimizations к сожалению требуется, не только -fno-signaling-nans, Я пробовал альтернативы + которые не могут вызвать исключения FPU, такие как ^ (xor) и | (или), но GCC не будет делать это на doubles. , Оператор не дает векторный результат для scalar , vector,

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

#ifndef NO_BROADCAST_SPECIALIZE
// specialized versions with initializer lists to work efficiently even without -ffast-math
// inline keyword prevents an actual definition from being emitted.
template<> inline A<2>::A (double v) {
    typeof (v_sse) val = {v, v};
    v_sse = val;
}
template<> inline A<4>::A (double v) {
    typeof (v_sse) val = {v, v, v, v};
    v_sse = val;
}
template<> inline A<8>::A (double v) {
    typeof (v_sse) val = {v, v, v, v, v, v, v, v};
    v_sse = val;
}
template<> inline A<16>::A (double v) { // AVX1024 or something may exist someday
    typeof (v_sse) val = {v, v, v, v, v, v, v, v, v, v, v, v, v, v, v, v};
    v_sse = val;
}
#endif

Теперь, чтобы проверить результаты:

// vecinit4 (from above) included in the asm output too.
// instantiate the templates
A<2> broadcast2(double val) { return A<2>(val); }
A<4> broadcast4(double val) { return A<4>(val); }
A<8> broadcast8(double val) { return A<8>(val); }

Вывод компилятора (директивы ассемблера удалены):

g++ -DNO_BROADCAST_SPECIALIZE  -O3 -Wall -mavx512f -march=native vec-gcc.cc -S -masm=intel -o-

_Z8vecinit4d:
    vbroadcastsd    ymm0, xmm0
    ret
_Z10broadcast2d:
    vmovddup        xmm1, xmm0
    vxorpd  xmm0, xmm0, xmm0
    vaddpd  xmm0, xmm1, xmm0
    ret
_Z10broadcast4d:
    vbroadcastsd    ymm1, xmm0
    vxorpd  xmm0, xmm0, xmm0
    vaddpd  ymm0, ymm1, ymm0
    ret
_Z10broadcast8d:
    vbroadcastsd    zmm0, xmm0
    vpxorq  zmm1, zmm1, zmm1
    vaddpd  zmm0, zmm0, zmm1
    ret


g++ -O3 -Wall -mavx512f -march=native vec-gcc.cc -S -masm=intel -o-
# or   g++ -ffast-math -DNO_BROADCAST_SPECIALIZE blah blah.

_Z8vecinit4d:
    vbroadcastsd    ymm0, xmm0
    ret
_Z10broadcast2d:
    vmovddup        xmm0, xmm0
    ret
_Z10broadcast4d:
    vbroadcastsd    ymm0, xmm0
    ret
_Z10broadcast8d:
    vbroadcastsd    zmm0, xmm0
    ret

Обратите внимание, что метод shuffle должен работать нормально, если вы не шаблонируете это, а вместо этого используете в своем коде только один векторный размер. Так что перейти с SSE на AVX так же просто, как с 16 на 32 в одном месте. Но тогда вам нужно будет скомпилировать один и тот же файл несколько раз, чтобы сгенерировать версию SSE и версию AVX, которую вы могли бы отправить во время выполнения. (Однако в любом случае это может понадобиться, чтобы иметь 128-битную версию SSE, в которой не используется кодировка инструкций VEX.)

Другие вопросы по тегам