Существуют ли переменные Move (_mm_move_ss) и Set (_mm_set_ss), которые работают для удвоений (__m128d)?

В течение многих лет я видел функции intrinsics с параметрами float, которые преобразуются в __m128 со следующим кодом: __m128 b = _mm_move_ss(m, _mm_set_ss(a));.

Например:

void MyFunction(float y)
{
    __m128 a = _mm_move_ss(m, _mm_set_ss(y)); //m is __m128
    //do whatever it is with 'a'
}

Интересно, есть ли аналогичный способ использования _mm_move и _mm_set intrinsics сделать то же самое для удвоений (__m128d)?

Ответы

Ответ 1

Почти каждая _ss и _ps внутренняя/инструкция имеет версию double с суффиксом _sd или _pd. (Scalar Double или Packed Double).

Например, выполните поиск (double встроенного искателя Intel, чтобы найти встроенные функции, которые принимают double как первый аргумент. Или просто выясните, какой оптимальный будет asm, а затем просмотрите внутренности для этих инструкций в руководстве insn ref. Кроме того, что не перечисляет все встроенные функции для movsd, поэтому поиск имени команды в поисковом устройстве intrinsics часто работает.

re: файлы заголовков: всегда включайте <immintrin.h>. Он включает все встроенные функции Intel SSE/AVX.


См. также способы поместить float в вектор, а sse теги wiki для ссылок о том, как перетасовывать векторы. (т.е. таблицы команд перетасовки в Agner Fog оптимизация руководства по сборке)

(см. ниже ссылку godbolt на какой-то интересный вывод компилятора)

re: ваша последовательность

Используйте только _mm_move_ss (или sd), если вы действительно хотите объединить два вектора.

Вы не показываете, как определяется m. Использование a в качестве имени переменной для float и вектора означает, что единственной полезной информацией в векторе является arg float. Разумеется, смена имени переменной означает, что она не компилируется.

К сожалению, похоже, что нет никакого способа просто "отличить" float или double от вектора с мусором в верхних 3 элементах, например, для __m128__m256:
__m256 _mm256_castps128_ps256 (__m128 a). Я задал новый вопрос об этом ограничении с помощью intrinsics: Как слить скаляр в вектор без компилятора, теряющего инструкцию обнуления верхних элементов? Ограничение разработки встроенных функций Intel?

Я попытался использовать _mm_undefined_ps() для достижения этого, надеясь, что это подскажет в компиляторе, что он может просто оставить входящий высокий мусор на месте, в

// don't use this, it doesn't make better code
__m128d double_to_vec_highgarbage(double x) {
  __m128d undef = _mm_undefined_pd();
  __m128d x_zeroupper = _mm_set_sd(x);
  return _mm_move_sd(undef, x_zeroupper);
}

но clang3.8 компилирует его в

    # clang3.8 -O3 -march=core2
    movq    xmm0, xmm0              # xmm0 = xmm0[0],zero
    ret

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

double_to_vec_highgarbage:  # gcc5.3 -march=nehalem
    movsd   QWORD PTR [rsp-16], xmm0      # %sfp, x
    movsd   xmm1, QWORD PTR [rsp-16]      # D.26885, %sfp
    pxor    xmm0, xmm0      # __Y
    movsd   xmm0, xmm1    # tmp93, D.26885
    ret

_mm_set_sd представляется лучшим способом превратить скаляр в вектор.

__m128d double_to_vec(double x) {
  return _mm_set_sd(x);
}

clang компилирует его в movq xmm0,xmm0, gcc для сохранения/перезагрузки с помощью -march=generic.


Другие интересные выходы компилятора из версий float и double в проводнике компилятора Godbolt

float_to_vec:   # gcc 5.3 -O3 -march=core2
    movd    eax, xmm0       # x, x
    movd    xmm0, eax       # D.26867, x
    ret

float_to_vec:   # gcc5.3 -O3 -march=nehalem
    insertps        xmm0, xmm0, 0xe # D.26867, x
    ret

double_to_vec:    # gcc5.3 -O3 -march=nehalem.  It could still have use movq or insertps, instead of this longer-latency store-forwarding round trip
    movsd   QWORD PTR [rsp-16], xmm0      # %sfp, x
    movsd   xmm0, QWORD PTR [rsp-16]      # D.26881, %sfp
    ret

float_to_vec:   # clang3.8 -O3 -march=core2 or generic (no -march)
    xorps   xmm1, xmm1
    movss   xmm1, xmm0              # xmm1 = xmm0[0],xmm1[1,2,3]
    movaps  xmm0, xmm1
    ret

double_to_vec:  # clang3.8 -O3 -march=core2, nehalem, or generic (no -march)
    movq    xmm0, xmm0              # xmm0 = xmm0[0],zero
    ret


float_to_vec:    # clang3.8 -O3 -march=nehalem
    xorps   xmm1, xmm1
    blendps xmm0, xmm1, 14          # xmm0 = xmm0[0],xmm1[1,2,3]
    ret

Таким образом, как clang, так и gcc используют разные стратегии для float vs. double, даже если они могут использовать ту же стратегию.

Использование целочисленных операций типа movq между операциями с плавающей запятой вызывает дополнительную задержку задержки байпаса. Используя insertps в ноль, верхние элементы входного регистра должны быть лучшей стратегией для float или double, поэтому все компиляторы должны использовать это, когда SSE4.1 доступен. xorps + blend тоже хорош и может работать на большем количестве портов, чем вставки. Магазин/перезагрузка, вероятно, является наихудшим, если только мы не узкополосны по пропускной способности ALU, и латентность не имеет значения.

Ответ 2

_mm_move_sd, _mm_set_sd. Это SSE2 intrinsics (а не SSE), поэтому вам понадобится #include <emmintrin.h>.