Самый быстрый способ заполнить вектор (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 не будет делать это на double
s. ,
Оператор не дает векторный результат для 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.)