Самый быстрый способ заполнить вектор (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 intrinsics.

Ответ 1

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

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

Для x86, мы можем получить gcc, чтобы испускать оптимальные трансляции для функции, которая принимает аргумент double (в 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 может транслироваться из памяти на лету:
    VADDPD zmm1 {k1}{z}, zmm2, zmm3/m512/m64bcst{er}
    {k1}{z} означает, что он может использовать регистр маски в качестве слияния или нулевой маски в результате.
    m64bcst означает 64-битный адрес памяти, который будет транслироваться.
    {er} означает, что режим округления MXCSR может быть переопределен для этой одной инструкции.
    IDK, если gcc будет использовать этот режим широковещательной адресации, чтобы сбросить широковещательные загрузки в операнды памяти.

Однако gcc также понимает перетасовку и имеет __builtin_shuffle для произвольных размеров вектора. С маской постоянной времени компиляции all-zeros тасование становится широковещательной передачей, которая 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 в настоящее время отправляет. Upstream, возможно, улучшился.)

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

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

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

#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.)